diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index cd90a80f..634f67c3 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -34,10 +34,15 @@ jobs: run: uv python install ${{ env.LATEST_PYTHON_VERSION }} - name: Install dependencies - run: uv sync --locked --all-extras --dev + run: uv sync --locked --all-extras --group dev - name: Run tests with coverage - run: uv run --no-sync pytest tests/unit + uses: nick-fields/retry@v3 + with: + timeout_minutes: 2 + max_attempts: 3 + command: uv run --no-sync pytest tests/unit + shell: bash - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 @@ -63,8 +68,7 @@ jobs: run: uv python install ${{ env.LATEST_PYTHON_VERSION }} - name: Install dependencies - run: uv sync --locked --all-extras --dev - shell: bash + run: uv sync --locked --all-extras --group dev - name: Run integration tests uses: nick-fields/retry@v3 diff --git a/.gitignore b/.gitignore index a9886a59..b8f59744 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,6 @@ cython_debug/ # Custom /junit.xml *.prof + +# Local logs +/logs/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4b84c553..33a2d4c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.10.0 + rev: 0.10.3 hooks: - id: uv-lock diff --git a/Dockerfile b/Dockerfile index de052584..cab06479 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,9 +38,9 @@ COPY --chmod=555 uv.lock ${WORKDIR} COPY --chmod=555 .env ${WORKDIR} RUN set -ex && \ - mkdir -p ${LOG_DIRECTORY} && \ + mkdir -p "${LOG_DIRECTORY}" && \ uv sync --frozen --no-dev && \ uv cache clean && \ - chown -R botuser:botuser ${WORKDIR} + chown -R botuser:botuser "${WORKDIR}" USER botuser diff --git a/README.md b/README.md index aa099a56..b336133e 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,308 @@ -
-

A Bot for Discord

-
- -
- - Donate - - - Sponsor - -
- -
- - Python - - - License: MIT - - - Code style: black - -
- -
- - codecov - - - Quality Gate Status - - - CI/CD Pipeline - -
- - -# [Install Guide - Wiki](https://ddc.github.io/DiscordBot) -+ Using Docker - + git clone https://github.com/ddc/DiscordBot.git - + BOT_TOKEN variable needs to be inside the .env file - + sudo systemctl enable docker - + docker-compose up --build -d - -# Games Included -+ [Guild Wars 2](https://www.guildwars2.com) - - -# OpenAI Command -| Commands | Description | -|:---------------|:------------------------------------------------| -| ai <_message_> | Asks OpenAI, message will be on discord embeded | - - -# Admin/Mod Commands -| Commands | Description | -|:---------------------------------------|:------------------------------------| -| admin cc [add,edit,remove] <_command_> | Add, edit or remove custom commands | -| admin botgame <_new game_> | Change game that bot is playing | - -# Config Commands -| Commands | Description | -|:-------------------------------------------------|:----------------------------------------------| -| admin config list | List all bot configurations | -| admin config servermessage [on , off] | Show message when a server gets updated | -| admin config membermessage [on , off] | Show message when someone updates the profile | -| admin config joinmessage [on , off] | Show message when a user joins the server | -| admin config leavemessage [on , off] | Show message when a user leaves the server | -| admin config blockinvisible [on , off] | Block messages from invisible members | -| admin config botreactions [on , off] | Bot will react to member words | -| admin config pfilter [on , off] <_channel name_> | Profanity Filter (blocks swear words) | - -# Misc Commands -| Commands | Description | -|:-------------------------|:-----------------------------------------| -| about | Displays bot info | -| echo | Shows your msg again | -| ping | Test latency by receiving a ping message | -| roll | Rolls random number | -| pepe | Posts a random Pepe from imgur url | -| tts <_message_> | Send TTS as .mp3 to channel | -| serverinfo | Shows server's informations | -| userinfo <_member#1234_> | Shows discord user informations | -| lmgtfy <_link_> | Creates a lmgtfy link | -| invites | List active invites link for the server | - -# Bot Owner Commands -| Commands | Description | -|:------------------------------------------|:--------------------------------| -| owner servers | Display all servers in database | -| owner prefix <_new prefix_> | Change bot prefix for commands | -| owner botdescription <_new description_> | Change bot description | - -# GW2 Commands -| Commands | Description | -|:------------------------------------------------|:---------------------------------------------| -| gw2 config list | List all gw2 configurations in the server | -| gw2 config session [on , off] | Bot should record users last sessions | -| gw2 wvw [match, info, kdr] <_world name_> | Info about a wvw match | -| gw2 key [add, update, remove, info] <_api key_> | Add/Update/Remove/Info - GW2 APIkey managing | -| gw2 account | General information about your GW2 account | -| gw2 worlds [na, eu] | List all worlds by timezone | -| gw2 wiki <_name to search_> | Search the Guild wars 2 wiki | -| gw2 info <_info to search_> | Information about a given name/skill/rune | +

+ DiscordBot +
+ DiscordBot +

+

+ Donate + Sponsor +
+ Python + uv + Ruff + License: MIT +
+ issues + codecov + Quality Gate Status + CI/CD Pipeline +

+

A simple Discord bot with OpenAI support and server administration tools.

-# Acknowledgements -+ [OpenAI API](https://openai.com/api) -+ [Guild Wars 2 API](https://wiki.guildwars2.com/wiki/API:2) -+ [Discord Bot Api](https://discordapp.com/developers/applications/me) -+ [PostgreSQL](https://www.postgresql.org) -+ [Git](https://git-scm.com/download) +## Table of Contents +- [Features](#features) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Configuration](#configuration) +- [Commands](#commands) + - [OpenAI](#openai-commands) + - [Admin/Mod](#adminmod-commands) + - [Config](#config-commands) + - [Custom Commands](#custom-commands) + - [Misc](#misc-commands) + - [Dice Rolls](#dice-rolls-commands) + - [Bot Owner](#bot-owner-commands) + - [GW2](#gw2-commands) + - [GW2 Config](#gw2-config-commands) + - [GW2 Key](#gw2-key-commands) + - [GW2 WvW](#gw2-wvw-commands) +- [Development and Testing](#development-and-testing) +- [Acknowledgements](#acknowledgements) +- [Support](#support) +- [License](#license) + + +# Features +- OpenAI integration for AI-powered responses +- Guild Wars 2 API integration (accounts, WvW, sessions, wiki) +- Server administration and moderation tools +- Custom commands, profanity filtering, and text-to-speech +- PostgreSQL database with Alembic migrations +- Docker deployment with automatic database migrations + + +# Prerequisites +- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) +- A [Discord Bot Token](https://discord.com/developers/applications) +- _(Optional)_ An [OpenAI API Key](https://platform.openai.com/api-keys) for AI commands +- _(Optional)_ A [Guild Wars 2 API Key](https://account.arena.net/applications) for GW2 commands + + +# Installation + +### 1. Clone the repository +```shell +git clone https://github.com/ddc/DiscordBot.git +cd DiscordBot +``` + +### 2. Configure environment variables +```shell +cp .env.example .env +``` + +Edit the `.env` file and set the required values: +```ini +# Required +BOT_TOKEN=your_discord_bot_token + +# Optional +OPENAI_API_KEY=your_openai_api_key + +# Database (defaults work with the included docker-compose) +POSTGRESQL_HOST=discordbot_database +POSTGRESQL_PORT=5432 +POSTGRESQL_USER=postgres +POSTGRESQL_PASSWORD=postgres +POSTGRESQL_DATABASE=discordbot +``` + +See [Configuration](#configuration) for all available options. + +### 3. Start the bot +```shell +sudo systemctl enable docker +docker-compose up --build -d +``` + +This will: +1. Build the Docker image +2. Start the PostgreSQL database +3. Run database migrations automatically +4. Start the bot + +### 4. Verify the bot is running +```shell +docker-compose logs -f discordbot +``` + +For the full installation guide, see the [Wiki](https://ddc.github.io/DiscordBot). + + +# Configuration + +All configuration is done through environment variables in the `.env` file. + +### Bot Settings +| Variable | Default | Description | +|:--------------------------|:------------------|:-----------------------------------------------------------| +| `BOT_TOKEN` | | Discord bot token **(required)** | +| `BOT_PREFIX` | `!` | Command prefix | +| `BOT_EMBED_COLOR` | `green` | Default embed color | +| `BOT_EMBED_OWNER_COLOR` | `dark_purple` | Owner command embed color | +| `BOT_ALLOWED_DM_COMMANDS` | `owner,about,gw2` | Commands allowed in DMs | +| `BOT_BOT_REACTION_WORDS` | `stupid,noob` | Words that trigger bot reactions | +| `BOT_EXCLUSIVE_USERS` | | Restrict bot to specific users (comma-separated IDs) | +| `BOT_BG_ACTIVITY_TIMER` | `0` | Background activity rotation timer (seconds, 0 = disabled) | + +### OpenAI Settings +| Variable | Default | Description | +|:-------------------|:--------------|:--------------------| +| `OPENAI_API_KEY` | | OpenAI API key | +| `BOT_OPENAI_MODEL` | `gpt-4o-mini` | OpenAI model to use | + +### PostgreSQL Settings +| Variable | Default | Description | +|:----------------------|:----------------------|:------------------| +| `POSTGRESQL_HOST` | `discordbot_database` | Database host | +| `POSTGRESQL_PORT` | `5432` | Database port | +| `POSTGRESQL_USER` | `postgres` | Database user | +| `POSTGRESQL_PASSWORD` | `postgres` | Database password | +| `POSTGRESQL_DATABASE` | `discordbot` | Database name | +| `POSTGRESQL_SCHEMA` | `public` | Database schema | +### Logging Settings +| Variable | Default | Description | +|:-------------------|:------------------|:----------------------| +| `LOG_LEVEL` | `INFO` | Log level | +| `LOG_TIMEZONE` | `UTC` | Log timezone | +| `LOG_DIRECTORY` | `/app/DiscordBot` | Log file directory | +| `LOG_DAYS_TO_KEEP` | `30` | Log retention in days | -## Development -Must have UV installed. See [UV Installation Guide](https://uv.run/docs/getting-started/installation) +See [.env.example](.env.example) for the complete list of configuration options including cooldowns, SSL, connection pooling, and retry settings. -### Building DEV Environment and Running Tests + +# Commands + +### OpenAI Commands +| Command | Description | +|:---------------|:-------------------------------------------------| +| `ai ` | Ask OpenAI for assistance, response as embed | + +### Admin/Mod Commands +| Command | Description | +|:---------------------------|:--------------------------------| +| `admin botgame ` | Change game that bot is playing | + +### Config Commands +| Command | Description | +|:-------------------------------------------|:---------------------------------------------| +| `admin config list` | List all bot configurations | +| `admin config joinmessage [on, off]` | Toggle message when a user joins the server | +| `admin config leavemessage [on, off]` | Toggle message when a user leaves the server | +| `admin config servermessage [on, off]` | Toggle message when a server gets updated | +| `admin config membermessage [on, off]` | Toggle message when someone updates profile | +| `admin config blockinvisible [on, off]` | Block messages from invisible members | +| `admin config botreactions [on, off]` | Toggle bot reactions to member words | +| `admin config pfilter [on, off] ` | Configure profanity filter per channel | + +### Custom Commands +| Command | Description | +|:--------------------------------------|:-----------------------------------| +| `admin cc add ` | Add a new custom command | +| `admin cc edit ` | Edit an existing custom command | +| `admin cc remove ` | Remove a custom command | +| `admin cc removeall` | Remove all custom commands | +| `admin cc list` | List all custom commands | + +### Misc Commands +| Command | Description | +|:--------------------|:----------------------------------------| +| `about` | Display bot info | +| `echo ` | Show your message again | +| `ping` | Test latency | +| `pepe` | Post a random Pepe image | +| `tts ` | Send text-to-speech as .mp3 to channel | +| `serverinfo` | Show server information | +| `userinfo ` | Show Discord user information | +| `lmgtfy ` | Create a LMGTFY link | +| `invites` | List active invite links for the server | + +### Dice Rolls Commands +| Command | Description | +|:-------------------------|:------------------------------------------| +| `roll` | Roll a die (defaults to 100) | +| `roll ` | Roll a die with specified size | +| `roll results` | Display all dice rolls from the server | +| `roll reset` | Delete all dice rolls (admin only) | + +### Bot Owner Commands +| Command | Description | +|:-----------------------------------------|:--------------------------------| +| `owner servers` | Display all servers in database | +| `owner prefix ` | Change bot prefix for commands | +| `owner botdescription ` | Update bot description | + +### GW2 Commands +| Command | Description | +|:--------------------|:------------------------------------------| +| `gw2 account` | Display your GW2 account information | +| `gw2 characters` | Display your GW2 characters information | +| `gw2 session` | Display your last game session data | +| `gw2 worlds na` | List all NA worlds with WvW tier | +| `gw2 worlds eu` | List all EU worlds with WvW tier | +| `gw2 wiki ` | Search the Guild Wars 2 wiki | +| `gw2 info ` | Information about a given name/skill/rune | + +### GW2 Config Commands +| Command | Description | +|:-------------------------------|:--------------------------------------| +| `gw2 config list` | List all GW2 configurations | +| `gw2 config session [on, off]` | Toggle recording of user sessions | + +### GW2 Key Commands +| Command | Description | +|:---------------------------|:------------------------------| +| `gw2 key add [api_key]` | Add your first GW2 API key | +| `gw2 key update [api_key]` | Update your existing API key | +| `gw2 key remove` | Remove your GW2 API key | +| `gw2 key info` | Show your API key information | + +### GW2 WvW Commands +| Command | Description | +|:------------------------|:-----------------------| +| `gw2 wvw info [world]` | Info about a WvW world | +| `gw2 wvw match [world]` | WvW match scores | +| `gw2 wvw kdr [world]` | WvW kill/death ratios | + + +# Development and Testing + +Requires [UV](https://docs.astral.sh/uv/getting-started/installation/) to be installed. + +### Setup ```shell -uv venv -uv sync --all-extras +uv sync --all-extras --all-groups +``` + +### Running Tests +```shell +# Unit tests poe test + +# Integration tests (requires Docker for testcontainers) +poe test-integration + +# All tests (unit + integration + hadolint + docker) +poe tests ``` +### Other Tasks +```shell +# Run linter (ruff) +poe linter + +# Update all dev dependencies +poe updatedev +# Run database migrations +poe migration -# License -Released under the [MIT](LICENSE). +# Profile unit tests +poe profile +# Profile integration tests +poe profile-integration +``` + + +# Acknowledgements +- [Discord Bot API](https://discord.com/developers/applications) +- [OpenAI API](https://openai.com/api) +- [Guild Wars 2 API](https://wiki.guildwars2.com/wiki/API:2) +- [PostgreSQL](https://www.postgresql.org) -# Buy me a cup of coffee -+ [GitHub Sponsor](https://github.com/sponsors/ddc) -+ [ko-fi](https://ko-fi.com/ddcsta) -+ [Paypal](https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ) +# Support +If you find this project helpful, consider supporting development: + +- [GitHub Sponsor](https://github.com/sponsors/ddc) +- [ko-fi](https://ko-fi.com/ddcsta) +- [PayPal](https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ) + + +# License +Released under the [MIT License](LICENSE) diff --git a/assets/DiscordBot-icon.svg b/assets/DiscordBot-icon.svg new file mode 100644 index 00000000..f9b23646 --- /dev/null +++ b/assets/DiscordBot-icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d094529c..acf5ec61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,66 +1,66 @@ [project] name = "DiscordBot" -version = "3.0.1" -description = "A Bot for Discord" +version = "3.0.2" +description = "A simple Discord bot with OpenAI support and server administration tools" urls.Repository = "https://github.com/ddc/DiscordBot" urls.Homepage = "https://github.com/ddc/DiscordBot" -license = {text = "MIT"} +license = { text = "MIT" } readme = "README.md" authors = [ - {name = "Daniel Costa", email = "ddcsoftwares@proton.me"}, + { name = "Daniel Costa", email = "ddcsoftwares@proton.me" }, ] maintainers = [ - {name = "Daniel Costa"}, + { name = "Daniel Costa" }, ] keywords = [ - "python", "python3", "python-3", - "DiscordBot", "discord-bots", "bot", - "discord-py", "discord" + "python", "python3", "python-3", + "DiscordBot", "discord-bots", "bot", + "discord-py", "discord" ] classifiers = [ - "Topic :: Communications :: Chat", - "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.14", - "Operating System :: OS Independent", - "Environment :: Other Environment", - "Intended Audience :: Developers", - "Natural Language :: English", + "Topic :: Communications :: Chat", + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "Environment :: Other Environment", + "Intended Audience :: Developers", + "Natural Language :: English", ] requires-python = ">=3.14" dependencies = [ - "alembic>=1.18.4", - "beautifulsoup4>=4.14.3", - "better-profanity>=0.7.0", - "ddcdatabases[postgres]>=3.0.10", - "discord-py>=2.6.4", - "gTTS>=2.5.4", - "openai>=2.20.0", - "PyNaCl>=1.6.2", - "pythonLogs>=6.0.2", + "alembic>=1.18.4", + "beautifulsoup4>=4.14.3", + "better-profanity>=0.7.0", + "ddcdatabases[postgres]>=3.0.10", + "discord-py>=2.6.4", + "gTTS>=2.5.4", + "openai>=2.21.0", + "PyNaCl>=1.6.2", + "pythonLogs>=6.0.2", ] [dependency-groups] dev = [ - "pytest-asyncio>=1.3.0", - "pytest-cov>=7.0.0", - "testcontainers[postgres]>=4.14.1", - "poethepoet>=0.41.0", - "ruff>=0.15.0", - "black>=26.1.0", + "pytest-asyncio>=1.3.0", + "pytest-cov>=7.0.0", + "testcontainers[postgres]>=4.14.1", + "poethepoet>=0.41.0", + "ruff>=0.15.1", ] [tool.poe.tasks] -linter.shell = "uv run ruff check --fix . && uv run black ." -profile.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit --no-cov"}] -profile-integration.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration --no-cov"}] -test.sequence = ["linter", {shell = "uv run pytest"}] -test-integration.sequence = ["linter", {shell = "uv run pytest tests/integration --no-cov"}] +linter.shell = "uv run ruff check --fix . && uv run ruff format ." +profile = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit --no-cov" +profile-integration = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration --no-cov" +test = "uv run pytest tests/unit" +test-integration = "uv run pytest tests/integration --no-cov" hadolint.shell = "docker run --rm -i -v $(pwd)/.hadolint.yaml:/.config/hadolint.yaml:ro hadolint/hadolint < Dockerfile" -test-docker.shell = "uv run pytest tests/docker -v --no-cov" +test-docker = "uv run pytest tests/docker -v --no-cov" +migration = "uv run --frozen alembic upgrade head" +tests.sequence = ["test", "test-integration", "hadolint", "test-docker"] updatedev.sequence = ["linter", {shell = "uv lock --upgrade && uv sync --all-extras --group dev"}] -migration.shell = "uv run --frozen alembic upgrade head" [tool.pytest.ini_options] addopts = "-v --import-mode=importlib --cov --cov-report=term --cov-report=xml --junitxml=junit.xml" @@ -70,35 +70,31 @@ testpaths = ["tests/unit"] asyncio_mode = "strict" asyncio_default_fixture_loop_scope = "function" markers = [ - "integration: marks tests as integration tests", - "docker: Docker and compose file tests", + "integration: marks tests as integration tests", + "docker: Docker and compose file tests", ] [tool.coverage.run] omit = [ - "tests/*", - "*/__init__.py", + "tests/*", + "*/__init__.py", ] [tool.coverage.report] show_missing = true skip_covered = false exclude_lines = [ - "pragma: no cover", - "def __repr__", - "if self.debug:", - "if settings.DEBUG", - "raise AssertionError", - "raise NotImplementedError", - "if 0:", - "if __name__ == .__main__.:", - "class .*\\bProtocol\\):", - "@(abc\\.)?abstractmethod", + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", ] -[tool.black] -line-length = 120 -skip-string-normalization = true - [tool.ruff] line-length = 120 target-version = "py314" diff --git a/src/__main__.py b/src/__main__.py index cd912639..6dc25d90 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -99,7 +99,7 @@ async def main() -> None: def run_bot() -> None: - print(messages.BOT_STARTING.format(variables.TIME_BEFORE_START)) + print(messages.bot_starting(variables.TIME_BEFORE_START)) time.sleep(variables.TIME_BEFORE_START) try: diff --git a/src/bot/cogs/admin/admin.py b/src/bot/cogs/admin/admin.py index 53fbf1e5..59119e92 100644 --- a/src/bot/cogs/admin/admin.py +++ b/src/bot/cogs/admin/admin.py @@ -44,7 +44,7 @@ async def botgame(self, ctx: commands.Context, *, game: str) -> None: await self.bot.change_presence(activity=activity) # Create success embed - embed = self._create_admin_embed(f"```{messages.BOT_ANNOUNCE_PLAYING.format(game)}```") + embed = self._create_admin_embed(f"```{messages.bot_announce_playing(game)}```") embed.set_author( name=self.bot.user.display_name, icon_url=self.bot.user.avatar.url if self.bot.user.avatar else None, @@ -58,7 +58,7 @@ async def _warn_about_bg_activity_timer(self, ctx: commands.Context) -> None: """Show warning if background activity timer will override the game status.""" bg_activity_timer = self.bot.settings["bot"]["BGActivityTimer"] if bg_activity_timer and bg_activity_timer > 0: - bg_task_warning = messages.BG_TASK_WARNING.format(bg_activity_timer) + bg_task_warning = messages.bg_task_warning(bg_activity_timer) warning_embed = self._create_admin_embed(bg_task_warning) await bot_utils.send_embed(ctx, warning_embed, False) diff --git a/src/bot/cogs/admin/config.py b/src/bot/cogs/admin/config.py index a769b471..2ef866b8 100644 --- a/src/bot/cogs/admin/config.py +++ b/src/bot/cogs/admin/config.py @@ -244,7 +244,7 @@ async def config_pfilter(ctx: commands.Context, *, subcommand_passed: str) -> No case _: raise commands.BadArgument(message="BadArgument") - msg = messages.CONFIG_PFILTER.format(status.upper(), channel.name) + msg = messages.config_pfilter(status.upper(), channel.name) embed = discord.Embed(description=msg, color=color) await bot_utils.send_embed(ctx, embed) return None @@ -273,7 +273,7 @@ async def config_list(ctx: commands.Context) -> None: # Format channel names if pf: - channel_names_lst = [channel['channel_name'] for channel in pf] + channel_names_lst = [channel["channel_name"] for channel in pf] channel_names = "\n".join(channel_names_lst) else: channel_names = messages.NO_CHANNELS_LISTED @@ -314,7 +314,7 @@ async def config_list(ctx: commands.Context) -> None: inline=True, ) embed.add_field( - name=f"🎭 {messages.CONFIG_BOT_WORD_REACTIONS.format(ctx.guild.name)}", + name=f"🎭 {messages.CONFIG_BOT_WORD_REACTIONS}", value=f"{on}" if sc["bot_word_reactions"] else f"{off}", inline=True, ) @@ -412,9 +412,9 @@ async def _handle_update( # Set updating state and disable all buttons self._updating = True for item in self.children: - if hasattr(item, 'disabled'): + if hasattr(item, "disabled"): item.disabled = True - if hasattr(item, 'style'): + if hasattr(item, "style"): item.style = discord.ButtonStyle.gray # Defer the response to allow editing the original message @@ -466,7 +466,7 @@ async def _handle_update( async def _restore_buttons(self): """Restore button states and colors.""" for item in self.children: - if hasattr(item, 'disabled'): + if hasattr(item, "disabled"): item.disabled = False # Restore original button colors @@ -500,7 +500,7 @@ async def _create_updated_embed(self): # Format channel names if pf: - channel_names_lst = [channel['channel_name'] for channel in pf] + channel_names_lst = [channel["channel_name"] for channel in pf] channel_names = "\n".join(channel_names_lst) else: channel_names = messages.NO_CHANNELS_LISTED @@ -543,7 +543,7 @@ async def _create_updated_embed(self): inline=True, ) embed.add_field( - name=f"🎭 {messages.CONFIG_BOT_WORD_REACTIONS.format(self.ctx.guild.name)}", + name=f"🎭 {messages.CONFIG_BOT_WORD_REACTIONS}", value=f"{on}" if self.server_config["bot_word_reactions"] else f"{off}", inline=True, ) diff --git a/src/bot/cogs/admin/custom_cmd.py b/src/bot/cogs/admin/custom_cmd.py index dd7b50b0..eb85bbd1 100644 --- a/src/bot/cogs/admin/custom_cmd.py +++ b/src/bot/cogs/admin/custom_cmd.py @@ -149,8 +149,7 @@ async def remove_custom_command(ctx: commands.Context, cmd_name: str) -> None: command_names = {cmd["name"] for cmd in server_commands} if cmd_name not in command_names: error_msg = ( - f"{messages.CUSTOM_COMMAND_UNABLE_REMOVE}\n" - f"{messages.COMMAND_NOT_FOUND}: {chat_formatting.inline(cmd_name)}" + f"{messages.CUSTOM_COMMAND_UNABLE_REMOVE}\n{messages.COMMAND_NOT_FOUND}: {chat_formatting.inline(cmd_name)}" ) return await bot_utils.send_error_msg(ctx, error_msg) diff --git a/src/bot/cogs/dice_rolls.py b/src/bot/cogs/dice_rolls.py index 2d4664d3..a1fba412 100644 --- a/src/bot/cogs/dice_rolls.py +++ b/src/bot/cogs/dice_rolls.py @@ -79,7 +79,7 @@ async def roll_results(self, ctx: commands.Context) -> None: server_rolls = await dice_rolls_dal.get_all_server_rolls(ctx.guild.id, dice_size) if not server_rolls: - return await bot_utils.send_error_msg(ctx, messages.NO_DICE_SIZE_ROLLS.format(dice_size)) + return await bot_utils.send_error_msg(ctx, messages.no_dice_size_rolls(dice_size)) embed = self._create_results_embed(ctx, server_rolls, dice_size) await bot_utils.send_embed(ctx, embed) diff --git a/src/bot/cogs/events/on_command.py b/src/bot/cogs/events/on_command.py index 968ef145..b6e49d54 100644 --- a/src/bot/cogs/events/on_command.py +++ b/src/bot/cogs/events/on_command.py @@ -55,22 +55,22 @@ def __init__(self, bot: Bot) -> None: self.bot = bot self.command_logger = CommandLogger(bot) - @self.bot.event - async def on_command(ctx: commands.Context) -> None: - """Handle command execution event. - - Called when a valid command gets executed successfully. - This is useful for logging command usage and monitoring. - - Args: - ctx: The command context containing information about the command execution - """ - try: - # Log command execution for monitoring purposes - self.command_logger.log_command_execution(ctx) - # Future: Add command usage statistics, rate limiting, etc. - except Exception as e: - self.bot.log.error(f"Error in on_command event: {e}") + @commands.Cog.listener() + async def on_command(self, ctx: commands.Context) -> None: + """Handle command execution event. + + Called when a valid command gets executed successfully. + This is useful for logging command usage and monitoring. + + Args: + ctx: The command context containing information about the command execution + """ + try: + # Log command execution for monitoring purposes + self.command_logger.log_command_execution(ctx) + # Future: Add command usage statistics, rate limiting, etc. + except Exception as e: + self.bot.log.error(f"Error in on_command event: {e}") async def setup(bot: Bot) -> None: diff --git a/src/bot/cogs/events/on_command_error.py b/src/bot/cogs/events/on_command_error.py index 62f456c0..8bcf0a4f 100644 --- a/src/bot/cogs/events/on_command_error.py +++ b/src/bot/cogs/events/on_command_error.py @@ -65,11 +65,13 @@ def build_missing_argument(context: ErrorContext) -> str: @staticmethod def build_check_failure(context: ErrorContext) -> str: """Build message for check failure error.""" - if "not admin" in context.error_msg: - return f"{messages.NOT_ADMIN_USE_COMMAND}: `{context.command}`" - elif "not owner" in context.error_msg: - return f"{messages.BOT_OWNERS_ONLY_COMMAND}: `{context.command}`" - return context.error_msg + match context.error_msg: + case msg if "not admin" in msg: + return f"{messages.NOT_ADMIN_USE_COMMAND}: `{context.command}`" + case msg if "not owner" in msg: + return f"{messages.BOT_OWNERS_ONLY_COMMAND}: `{context.command}`" + case _: + return context.error_msg @staticmethod def build_bad_argument(context: ErrorContext) -> str: @@ -97,8 +99,8 @@ def build_command_invoke_error(context: ErrorContext) -> str: "NoOptionError", ): f"{messages.NO_OPTION_FOUND}: `{context.error_msg.split()[7] if len(context.error_msg.split()) > 7 else 'unknown'}`", ("GW2 API",): ( - str(context.error_msg).split(',')[1].strip().split('?')[0] - if ',' in str(context.error_msg) and len(str(context.error_msg).split(',')) > 1 + str(context.error_msg).split(",")[1].strip().split("?")[0] + if "," in str(context.error_msg) and len(str(context.error_msg).split(",")) > 1 else str(context.error_msg) ), ("No text to send to TTS API",): messages.INVALID_MESSAGE, @@ -184,13 +186,14 @@ async def _handle_check_failure(self, context: ErrorContext, should_log: bool) - async def _handle_bad_argument(self, context: ErrorContext, should_log: bool) -> None: """Handle BadArgument error.""" # Extract bad argument from message content - if context.error_msg == "BadArgument_Gw2ConfigStatus": - context.bad_argument = context.ctx.message.clean_content.split()[3] - elif context.error_msg == "BadArgument_Gw2ConfigServer": - bad_server_list = context.ctx.message.clean_content.split()[4:] - context.bad_argument = " ".join(bad_server_list) - else: - context.bad_argument = context.ctx.message.clean_content.replace(context.command, "").strip() + match context.error_msg: + case "BadArgument_Gw2ConfigStatus": + context.bad_argument = context.ctx.message.clean_content.split()[3] + case "BadArgument_Gw2ConfigServer": + bad_server_list = context.ctx.message.clean_content.split()[4:] + context.bad_argument = " ".join(bad_server_list) + case _: + context.bad_argument = context.ctx.message.clean_content.replace(context.command, "").strip() error_msg = self.message_builder.build_bad_argument(context) await self._send_error_message(context.ctx, error_msg, should_log) diff --git a/src/bot/cogs/events/on_connect.py b/src/bot/cogs/events/on_connect.py index 57546c20..991789a1 100644 --- a/src/bot/cogs/events/on_connect.py +++ b/src/bot/cogs/events/on_connect.py @@ -134,22 +134,22 @@ def __init__(self, bot: Bot) -> None: self.bot = bot self.connection_handler = ConnectionHandler(bot) - @self.bot.event - async def on_connect() -> None: - """Handle bot connection event. - - Called when the client has successfully connected to Discord. - This is not the same as the client being fully prepared (see on_ready for that). - - This event handles: - - Database synchronization - - Guild verification - - Connection logging - """ - try: - await self.connection_handler.process_connection() - except Exception as e: - self.bot.log.error(f"Critical error in on_connect event: {e}") + @commands.Cog.listener() + async def on_connect(self) -> None: + """Handle bot connection event. + + Called when the client has successfully connected to Discord. + This is not the same as the client being fully prepared (see on_ready for that). + + This event handles: + - Database synchronization + - Guild verification + - Connection logging + """ + try: + await self.connection_handler.process_connection() + except Exception as e: + self.bot.log.error(f"Critical error in on_connect event: {e}") async def setup(bot: Bot) -> None: diff --git a/src/bot/cogs/events/on_disconnect.py b/src/bot/cogs/events/on_disconnect.py index 6f83f5ab..ae1d8fb6 100644 --- a/src/bot/cogs/events/on_disconnect.py +++ b/src/bot/cogs/events/on_disconnect.py @@ -14,26 +14,22 @@ def __init__(self, bot: Bot) -> None: """ self.bot = bot - @self.bot.event - async def on_disconnect() -> None: - """Handle bot disconnect event. - - Called when the client has disconnected from Discord, - or a connection attempt to Discord has failed. - This could happen through: - - Internet disconnection - - Explicit calls to close - - Discord terminating the connection - """ - try: - bot.log.warning( - messages.BOT_DISCONNECTED.format(bot.user) - if hasattr(messages, 'BOT_DISCONNECTED') - else f"Bot {bot.user} disconnected from Discord" - ) - except Exception as e: - # Fallback logging in case of critical failure - print(f"Bot disconnected - logging failed: {e}") + @commands.Cog.listener() + async def on_disconnect(self) -> None: + """Handle bot disconnect event. + + Called when the client has disconnected from Discord, + or a connection attempt to Discord has failed. + This could happen through: + - Internet disconnection + - Explicit calls to close + - Discord terminating the connection + """ + try: + self.bot.log.warning(messages.bot_disconnected(self.bot.user)) + except Exception as e: + # Fallback logging in case of critical failure + print(f"Bot disconnected - logging failed: {e}") async def setup(bot: Bot) -> None: diff --git a/src/bot/cogs/events/on_guild_channel_create.py b/src/bot/cogs/events/on_guild_channel_create.py index b4f2b976..24a84871 100644 --- a/src/bot/cogs/events/on_guild_channel_create.py +++ b/src/bot/cogs/events/on_guild_channel_create.py @@ -5,14 +5,14 @@ class OnGuildChannelCreate(commands.Cog): def __init__(self, bot): self.bot = bot - @self.bot.event - async def on_guild_channel_create(channel): - """ - Called when a channel gets created - :param channel: abc.GuildChannel - :return: None - """ - pass + @commands.Cog.listener() + async def on_guild_channel_create(self, channel): + """ + Called when a channel gets created + :param channel: abc.GuildChannel + :return: None + """ + pass async def setup(bot): diff --git a/src/bot/cogs/events/on_guild_channel_delete.py b/src/bot/cogs/events/on_guild_channel_delete.py index 73b2c102..a3f289e9 100644 --- a/src/bot/cogs/events/on_guild_channel_delete.py +++ b/src/bot/cogs/events/on_guild_channel_delete.py @@ -5,14 +5,14 @@ class OnGuildChannelDelete(commands.Cog): def __init__(self, bot): self.bot = bot - @self.bot.event - async def on_guild_channel_delete(channel): - """ - Called when a channel gets deleted - :param channel: abc.GuildChannel - :return: None - """ - pass + @commands.Cog.listener() + async def on_guild_channel_delete(self, channel): + """ + Called when a channel gets deleted + :param channel: abc.GuildChannel + :return: None + """ + pass async def setup(bot): diff --git a/src/bot/cogs/events/on_guild_channel_update.py b/src/bot/cogs/events/on_guild_channel_update.py index 3e714c99..f0cf9b93 100644 --- a/src/bot/cogs/events/on_guild_channel_update.py +++ b/src/bot/cogs/events/on_guild_channel_update.py @@ -5,15 +5,15 @@ class OnGuildChannelUpdate(commands.Cog): def __init__(self, bot): self.bot = bot - @self.bot.event - async def on_guild_channel_update(before, after): - """ - Called when a channel gets updated - :param before: abc.GuildChannel - :param after: abc.GuildChannel - :return: None - """ - pass + @commands.Cog.listener() + async def on_guild_channel_update(self, before, after): + """ + Called when a channel gets updated + :param before: abc.GuildChannel + :param after: abc.GuildChannel + :return: None + """ + pass async def setup(bot): diff --git a/src/bot/cogs/events/on_guild_join.py b/src/bot/cogs/events/on_guild_join.py index ff05c6f9..5cb1f5b5 100644 --- a/src/bot/cogs/events/on_guild_join.py +++ b/src/bot/cogs/events/on_guild_join.py @@ -12,7 +12,7 @@ class WelcomeMessageBuilder: @staticmethod def build_welcome_message(bot_name: str, prefix: str, games_included: str) -> str: """Build the welcome message text.""" - return messages.GUILD_JOIN_BOT_MESSAGE.format(bot_name, prefix, games_included, prefix, prefix) + return messages.guild_join_bot_message(bot_name, prefix, games_included) @staticmethod def build_welcome_embed(bot: Bot, message: str) -> discord.Embed: @@ -41,7 +41,7 @@ def _set_footer(embed: discord.Embed, bot: Bot) -> None: """Set footer with developer information.""" try: author = bot.get_user(bot.owner_id) - python_version = "Python {}.{}.{}".format(*sys.version_info[:3]) + python_version = f"Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" if author and author.avatar: embed.set_footer(icon_url=author.avatar.url, text=f"Developed by {author} | {python_version}") @@ -49,7 +49,7 @@ def _set_footer(embed: discord.Embed, bot: Bot) -> None: embed.set_footer(text=f"Developed by Bot Owner | {python_version}") except AttributeError, discord.HTTPException: # Fallback if owner information is not available or HTTP error occurs - python_version = "Python {}.{}.{}".format(*sys.version_info[:3]) + python_version = f"Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" embed.set_footer(text=f"Discord Bot | {python_version}") @@ -60,23 +60,23 @@ def __init__(self, bot: Bot) -> None: self.bot = bot self.message_builder = WelcomeMessageBuilder() - @self.bot.event - async def on_guild_join(guild: discord.Guild) -> None: - """Handle bot joining a new guild.""" - # Register server in database - await bot_utils.insert_server(bot, guild) - - # Build welcome message and embed - games_included = "".join(variables.GAMES_INCLUDED) - welcome_text = self.message_builder.build_welcome_message( - bot.user.name, - bot.command_prefix, - games_included, - ) - welcome_embed = self.message_builder.build_welcome_embed(bot, welcome_text) - - # Send welcome message to system channel - await bot_utils.send_msg_to_system_channel(bot.log, guild, welcome_embed, welcome_text) + @commands.Cog.listener() + async def on_guild_join(self, guild: discord.Guild) -> None: + """Handle bot joining a new guild.""" + # Register server in database + await bot_utils.insert_server(self.bot, guild) + + # Build welcome message and embed + games_included = "".join(variables.GAMES_INCLUDED) + welcome_text = self.message_builder.build_welcome_message( + self.bot.user.name, + self.bot.command_prefix, + games_included, + ) + welcome_embed = self.message_builder.build_welcome_embed(self.bot, welcome_text) + + # Send welcome message to system channel + await bot_utils.send_msg_to_system_channel(self.bot.log, guild, welcome_embed, welcome_text) async def setup(bot: Bot) -> None: diff --git a/src/bot/cogs/events/on_guild_remove.py b/src/bot/cogs/events/on_guild_remove.py index f77583bb..8b755f3f 100644 --- a/src/bot/cogs/events/on_guild_remove.py +++ b/src/bot/cogs/events/on_guild_remove.py @@ -47,26 +47,26 @@ def __init__(self, bot: Bot) -> None: self.bot = bot self.cleanup_handler = GuildCleanupHandler(bot) - @self.bot.event - async def on_guild_remove(guild: discord.Guild) -> None: - """Handle guild removal event. - - Called when the bot is removed from a guild (server). - This includes being kicked, banned, or the server being deleted. - - Args: - guild: The Discord guild the bot was removed from - """ - try: - self.bot.log.info(f"Bot removed from guild: {guild.name} (ID: {guild.id})") - - # Clean up server data from database - cleanup_success = await self.cleanup_handler.cleanup_server_data(guild) - - if not cleanup_success: - self.bot.log.warning(f"Database cleanup may be incomplete for guild: {guild.name}") - except Exception as e: - self.bot.log.error(f"Error handling guild removal for {guild.name}: {e}") + @commands.Cog.listener() + async def on_guild_remove(self, guild: discord.Guild) -> None: + """Handle guild removal event. + + Called when the bot is removed from a guild (server). + This includes being kicked, banned, or the server being deleted. + + Args: + guild: The Discord guild the bot was removed from + """ + try: + self.bot.log.info(f"Bot removed from guild: {guild.name} (ID: {guild.id})") + + # Clean up server data from database + cleanup_success = await self.cleanup_handler.cleanup_server_data(guild) + + if not cleanup_success: + self.bot.log.warning(f"Database cleanup may be incomplete for guild: {guild.name}") + except Exception as e: + self.bot.log.error(f"Error handling guild removal for {guild.name}: {e}") async def setup(bot: Bot) -> None: diff --git a/src/bot/cogs/events/on_guild_update.py b/src/bot/cogs/events/on_guild_update.py index d1331f96..e01fc49e 100644 --- a/src/bot/cogs/events/on_guild_update.py +++ b/src/bot/cogs/events/on_guild_update.py @@ -17,29 +17,29 @@ def __init__(self, bot: Bot) -> None: """ self.bot = bot - @self.bot.event - async def on_guild_update(before: discord.Guild, after: discord.Guild) -> None: - """Handle guild update event. + @commands.Cog.listener() + async def on_guild_update(self, before: discord.Guild, after: discord.Guild) -> None: + """Handle guild update event. - Called when a guild is updated (name, icon, owner changes, etc.). + Called when a guild is updated (name, icon, owner changes, etc.). - Args: - before: The guild before the update - after: The guild after the update - """ - try: - embed, msg = self._create_base_embed() + Args: + before: The guild before the update + after: The guild after the update + """ + try: + embed, msg = self._create_base_embed() - # Check for changes and update embed/message - self._handle_icon_changes(before, after, embed, msg) - self._handle_name_changes(before, after, embed, msg) - self._handle_owner_changes(before, after, embed, msg) + # Check for changes and update embed/message + self._handle_icon_changes(before, after, embed, msg) + self._handle_name_changes(before, after, embed, msg) + self._handle_owner_changes(before, after, embed, msg) - # Send notification if changes were detected - await self._send_notification_if_enabled(after, embed, msg) + # Send notification if changes were detected + await self._send_notification_if_enabled(after, embed, msg) - except Exception as e: - self.bot.log.error(f"Error in on_guild_update for {after.name}: {e}") + except Exception as e: + self.bot.log.error(f"Error in on_guild_update for {after.name}: {e}") def _create_base_embed(self): """Create the base embed and message for guild updates.""" @@ -63,7 +63,7 @@ def _handle_icon_changes(self, before, after, embed, msg): if str(before.icon.url) != str(after.icon.url): self._set_thumbnail_if_icon_exists(after, embed) embed.add_field(name=messages.NEW_SERVER_ICON, value="") - icon_url = after.icon.url if after.icon else 'None' + icon_url = after.icon.url if after.icon else "None" msg.append(f"{messages.NEW_SERVER_ICON}: \n{icon_url}\n") @staticmethod diff --git a/src/bot/cogs/events/on_member_join.py b/src/bot/cogs/events/on_member_join.py index ffdc8030..cc984f77 100644 --- a/src/bot/cogs/events/on_member_join.py +++ b/src/bot/cogs/events/on_member_join.py @@ -125,21 +125,21 @@ def __init__(self, bot: Bot) -> None: self.bot = bot self.join_handler = MemberJoinHandler(bot) - @self.bot.event - async def on_member_join(member: discord.Member) -> None: - """Handle member join event. - - Called when a member joins a guild where the bot is present. - Sends welcome messages if configured to do so. - - Args: - member: The Discord member who joined the guild - """ - try: - self.bot.log.info(f"Member joined: {member} in guild: {member.guild.name}") - await self.join_handler.process_member_join(member) - except Exception as e: - self.bot.log.error(f"Critical error in on_member_join for {member}: {e}") + @commands.Cog.listener() + async def on_member_join(self, member: discord.Member) -> None: + """Handle member join event. + + Called when a member joins a guild where the bot is present. + Sends welcome messages if configured to do so. + + Args: + member: The Discord member who joined the guild + """ + try: + self.bot.log.info(f"Member joined: {member} in guild: {member.guild.name}") + await self.join_handler.process_member_join(member) + except Exception as e: + self.bot.log.error(f"Critical error in on_member_join for {member}: {e}") async def setup(bot: Bot) -> None: diff --git a/src/bot/cogs/events/on_member_remove.py b/src/bot/cogs/events/on_member_remove.py index 056485e0..9452aef3 100644 --- a/src/bot/cogs/events/on_member_remove.py +++ b/src/bot/cogs/events/on_member_remove.py @@ -129,22 +129,22 @@ def __init__(self, bot: Bot) -> None: self.bot = bot self.leave_handler = MemberLeaveHandler(bot) - @self.bot.event - async def on_member_remove(member: discord.Member) -> None: - """Handle member remove event. - - Called when a member leaves a guild where the bot is present. - This includes being kicked, banned, or leaving voluntarily. - Sends farewell messages if configured to do so. - - Args: - member: The Discord member who left the guild - """ - try: - self.bot.log.info(f"Member left: {member} from guild: {member.guild.name}") - await self.leave_handler.process_member_leave(member) - except Exception as e: - self.bot.log.error(f"Critical error in on_member_remove for {member}: {e}") + @commands.Cog.listener() + async def on_member_remove(self, member: discord.Member) -> None: + """Handle member remove event. + + Called when a member leaves a guild where the bot is present. + This includes being kicked, banned, or leaving voluntarily. + Sends farewell messages if configured to do so. + + Args: + member: The Discord member who left the guild + """ + try: + self.bot.log.info(f"Member left: {member} from guild: {member.guild.name}") + await self.leave_handler.process_member_leave(member) + except Exception as e: + self.bot.log.error(f"Critical error in on_member_remove for {member}: {e}") async def setup(bot: Bot) -> None: diff --git a/src/bot/cogs/events/on_member_update.py b/src/bot/cogs/events/on_member_update.py index 2f4e224b..c9f48c2d 100644 --- a/src/bot/cogs/events/on_member_update.py +++ b/src/bot/cogs/events/on_member_update.py @@ -17,36 +17,36 @@ def __init__(self, bot: Bot) -> None: """ self.bot = bot - @self.bot.event - async def on_member_update(before: discord.Member, after: discord.Member) -> None: - """Handle member update event. - - Called when a Member updates their profile. - This includes changes to: - - nickname - - roles - - pending status - - flags - - Args: - before: The member before the update - after: The member after the update - """ - try: - if after.bot: - return - - embed, msg = self._create_member_embed(after) - - # Check for changes and update embed/message - self._handle_nickname_changes(before, after, embed, msg) - self._handle_role_changes(before, after, embed, msg) - - # Send notification if changes were detected - await self._send_notification_if_enabled(after, embed, msg) - - except Exception as e: - self.bot.log.error(f"Error in on_member_update for {after}: {e}") + @commands.Cog.listener() + async def on_member_update(self, before: discord.Member, after: discord.Member) -> None: + """Handle member update event. + + Called when a Member updates their profile. + This includes changes to: + - nickname + - roles + - pending status + - flags + + Args: + before: The member before the update + after: The member after the update + """ + try: + if after.bot: + return + + embed, msg = self._create_member_embed(after) + + # Check for changes and update embed/message + self._handle_nickname_changes(before, after, embed, msg) + self._handle_role_changes(before, after, embed, msg) + + # Send notification if changes were detected + await self._send_notification_if_enabled(after, embed, msg) + + except Exception as e: + self.bot.log.error(f"Error in on_member_update for {after}: {e}") def _create_member_embed(self, member): """Create the base embed and message for member updates.""" diff --git a/src/bot/cogs/events/on_message.py b/src/bot/cogs/events/on_message.py index 8224d2cc..e6939ae0 100644 --- a/src/bot/cogs/events/on_message.py +++ b/src/bot/cogs/events/on_message.py @@ -305,7 +305,7 @@ async def _handle_invisible_member(self, ctx: commands.Context) -> None: """Handle message from invisible member.""" await bot_utils.delete_message(ctx) - message_text = messages.BLOCKED_INVIS_MESSAGE.format(ctx.guild.name) + message_text = messages.blocked_invis_message(ctx.guild.name) embed = discord.Embed( title="", color=discord.Color.red(), diff --git a/src/bot/cogs/events/on_presence_update.py b/src/bot/cogs/events/on_presence_update.py index 531134cc..d65c6557 100644 --- a/src/bot/cogs/events/on_presence_update.py +++ b/src/bot/cogs/events/on_presence_update.py @@ -6,21 +6,21 @@ class OnPresenceUpdate(commands.Cog): def __init__(self, bot): self.bot = bot - @self.bot.event - async def on_presence_update(before, after): - """ - Called when a Member updates their presence. - This is called when one or more of the following things change: - status - activity - :param before: discord.Member - :param after: discord.Member - :return: None - """ - if after.bot: - return + @commands.Cog.listener() + async def on_presence_update(self, before, after): + """ + Called when a Member updates their presence. + This is called when one or more of the following things change: + status + activity + :param before: discord.Member + :param after: discord.Member + :return: None + """ + if after.bot: + return - await gw2_utils.check_gw2_game_activity(bot, before, after) + await gw2_utils.check_gw2_game_activity(self.bot, before, after) async def setup(bot): diff --git a/src/bot/cogs/events/on_ready.py b/src/bot/cogs/events/on_ready.py index 668d42df..94adb26c 100644 --- a/src/bot/cogs/events/on_ready.py +++ b/src/bot/cogs/events/on_ready.py @@ -18,7 +18,7 @@ def print_startup_banner(version: str) -> None: @staticmethod def print_version_info() -> None: """Print version information for Python and Discord API.""" - python_version = "Python v{}.{}.{}".format(*sys.version_info[:3]) + python_version = f"Python v{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" discord_version = f"Discord API v{discord.__version__}" print(python_version) print(discord_version) @@ -51,32 +51,32 @@ def __init__(self, bot: Bot) -> None: self.bot = bot self.info_display = StartupInfoDisplay() - @self.bot.event - async def on_ready() -> None: - """Handle bot ready event with comprehensive startup information.""" - try: - # Get bot statistics - bot_stats = bot_utils.get_bot_stats(bot) - - # Display startup information - self.info_display.print_startup_banner(variables.VERSION) - self.info_display.print_version_info() - self.info_display.print_bot_info(bot) - self.info_display.print_bot_stats(bot_stats) - self.info_display.print_timestamp() - - # Log bot online status - bot.log.info(messages.BOT_ONLINE.format(bot.user)) - except Exception as e: - # Display startup information even if stats fail - self.info_display.print_startup_banner(variables.VERSION) - self.info_display.print_version_info() - self.info_display.print_bot_info(bot) - self.info_display.print_timestamp() - - # Log error and bot online status - bot.log.error(f"Failed to get bot stats during startup: {e}") - bot.log.info(messages.BOT_ONLINE.format(bot.user)) + @commands.Cog.listener() + async def on_ready(self) -> None: + """Handle bot ready event with comprehensive startup information.""" + try: + # Get bot statistics + bot_stats = bot_utils.get_bot_stats(self.bot) + + # Display startup information + self.info_display.print_startup_banner(variables.VERSION) + self.info_display.print_version_info() + self.info_display.print_bot_info(self.bot) + self.info_display.print_bot_stats(bot_stats) + self.info_display.print_timestamp() + + # Log bot online status + self.bot.log.info(messages.bot_online(self.bot.user)) + except Exception as e: + # Display startup information even if stats fail + self.info_display.print_startup_banner(variables.VERSION) + self.info_display.print_version_info() + self.info_display.print_bot_info(self.bot) + self.info_display.print_timestamp() + + # Log error and bot online status + self.bot.log.error(f"Failed to get bot stats during startup: {e}") + self.bot.log.info(messages.bot_online(self.bot.user)) async def setup(bot: Bot) -> None: diff --git a/src/bot/cogs/events/on_user_update.py b/src/bot/cogs/events/on_user_update.py index 6c44ed41..ee80dd17 100644 --- a/src/bot/cogs/events/on_user_update.py +++ b/src/bot/cogs/events/on_user_update.py @@ -8,53 +8,53 @@ class OnUserUpdate(commands.Cog): def __init__(self, bot): self.bot = bot - @self.bot.event - async def on_user_update(before, after): - """ - Called when a User updates their profile. - This is called before on_member_update event is triggered - This is called when one or more of the following things change: - avatar - username - discriminator - :param before: discord.Member - :param after: discord.Member - :return: None - """ - if after.bot: - return - - msg = f"{messages.PROFILE_CHANGES}:\n\n" - embed = bot_utils.get_embed(self) - embed.set_author(name=after.display_name, icon_url=after.avatar.url) - embed.set_footer( - icon_url=self.bot.user.avatar.url, - text=f"{bot_utils.get_current_date_time_str_long()} UTC", - ) - - if str(before.avatar.url) != str(after.avatar.url): - embed.set_thumbnail(url=after.avatar.url) - embed.add_field(name=messages.NEW_AVATAR, value="-->") - msg += f"{messages.NEW_AVATAR}: \n{after.avatar.url}\n" - - if str(before.name) != str(after.name): - if before.name is not None: - embed.add_field(name=messages.PREVIOUS_NAME, value=str(before.name)) - embed.add_field(name=messages.NEW_NAME, value=str(after.name)) - msg += f"{messages.NEW_NAME}: `{after.name}`\n" - - if str(before.discriminator) != str(after.discriminator): - if before.name is not None: - embed.add_field(name=messages.PREVIOUS_DISCRIMINATOR, value=str(before.discriminator)) - embed.add_field(name=messages.NEW_DISCRIMINATOR, value=str(after.discriminator)) - msg += f"{messages.NEW_DISCRIMINATOR}: `{after.discriminator}`\n" - - if len(embed.fields) > 0: - servers_dal = ServersDal(bot.db_session, bot.log) - for guild in after.mutual_guilds: - rs = await servers_dal.get_server(guild.id) - if rs["msg_on_member_update"]: - await bot_utils.send_msg_to_system_channel(self.bot.log, guild, embed, msg) + @commands.Cog.listener() + async def on_user_update(self, before, after): + """ + Called when a User updates their profile. + This is called before on_member_update event is triggered + This is called when one or more of the following things change: + avatar + username + discriminator + :param before: discord.Member + :param after: discord.Member + :return: None + """ + if after.bot: + return + + msg = f"{messages.PROFILE_CHANGES}:\n\n" + embed = bot_utils.get_embed(self) + embed.set_author(name=after.display_name, icon_url=after.avatar.url) + embed.set_footer( + icon_url=self.bot.user.avatar.url, + text=f"{bot_utils.get_current_date_time_str_long()} UTC", + ) + + if str(before.avatar.url) != str(after.avatar.url): + embed.set_thumbnail(url=after.avatar.url) + embed.add_field(name=messages.NEW_AVATAR, value="-->") + msg += f"{messages.NEW_AVATAR}: \n{after.avatar.url}\n" + + if str(before.name) != str(after.name): + if before.name is not None: + embed.add_field(name=messages.PREVIOUS_NAME, value=str(before.name)) + embed.add_field(name=messages.NEW_NAME, value=str(after.name)) + msg += f"{messages.NEW_NAME}: `{after.name}`\n" + + if str(before.discriminator) != str(after.discriminator): + if before.name is not None: + embed.add_field(name=messages.PREVIOUS_DISCRIMINATOR, value=str(before.discriminator)) + embed.add_field(name=messages.NEW_DISCRIMINATOR, value=str(after.discriminator)) + msg += f"{messages.NEW_DISCRIMINATOR}: `{after.discriminator}`\n" + + if len(embed.fields) > 0: + servers_dal = ServersDal(self.bot.db_session, self.bot.log) + for guild in after.mutual_guilds: + rs = await servers_dal.get_server(guild.id) + if rs["msg_on_member_update"]: + await bot_utils.send_msg_to_system_channel(self.bot.log, guild, embed, msg) async def setup(bot): diff --git a/src/bot/cogs/misc.py b/src/bot/cogs/misc.py index be6f04c9..159fdfdb 100644 --- a/src/bot/cogs/misc.py +++ b/src/bot/cogs/misc.py @@ -222,7 +222,7 @@ async def about(self, ctx: commands.Context) -> None: author = self.bot.get_user(self.bot.owner_id) python_version = f"Python {'.'.join(map(str, sys.version_info[:3]))}" games_included = self._get_games_included(variables.GAMES_INCLUDED) - dev_info_msg = messages.DEV_INFO_MSG.format(variables.BOT_WEBPAGE_URL, variables.DISCORDPY_URL) + dev_info_msg = messages.dev_info_msg(variables.BOT_WEBPAGE_URL, variables.DISCORDPY_URL) bot_stats = bot_utils.get_bot_stats(self.bot) diff --git a/src/bot/constants/messages.py b/src/bot/constants/messages.py index 96204828..835f8a92 100644 --- a/src/bot/constants/messages.py +++ b/src/bot/constants/messages.py @@ -1,9 +1,7 @@ ################################# # BOT ################################# -BOT_ONLINE = "====> {0} IS ONLINE AND CONNECTED TO DISCORD <====" BOT_TOKEN_NOT_FOUND = "BOT_TOKEN variable not found" -BOT_STARTING = "Starting Bot in {0} secs" BOT_TERMINATED = "Bot has been terminated." BOT_STOPPED_CTRTC = "Bot stopped with Ctrl+C" BOT_FATAL_ERROR_MAIN = "Fatal error in main()" @@ -14,11 +12,33 @@ BOT_LOAD_SETTINGS_FAILED = "Failed to load settings" BOT_LOAD_COGS_FAILED = "Failed to load cogs" BOT_LOADED_ALL_COGS_SUCCESS = "Successfully loaded all cogs" + + +def bot_online(bot_user) -> str: + return f"====> {bot_user} IS ONLINE AND CONNECTED TO DISCORD <====" + + +def bot_starting(seconds: int) -> str: + return f"Starting Bot in {seconds} secs" + + +def bot_disconnected(bot_user) -> str: + return f"Bot {bot_user} disconnected from Discord" + + ################################# # EVENT ADMIN ################################# -BOT_ANNOUNCE_PLAYING = "I'm now playing: {0}" -BG_TASK_WARNING = "Background task running to update bot activity is ON\nActivity will change after {0} secs." + + +def bot_announce_playing(game: str) -> str: + return f"I'm now playing: {game}" + + +def bg_task_warning(seconds: int) -> str: + return f"Background task running to update bot activity is ON\nActivity will change after {seconds} secs." + + ################################# # EVENT CONFIG ################################# @@ -28,13 +48,18 @@ CONFIG_MEMBER = "Display a message when someone changes profile" CONFIG_BLOCK_INVIS_MEMBERS = "Block messages from invisible members" CONFIG_BOT_WORD_REACTIONS = "Bot word reactions" -CONFIG_PFILTER = "Profanity Filter `{0}`\nChannel: `{1}`" CONFIG_PFILTER_CHANNELS = "Channels with profanity filter activated" + + +def config_pfilter(status: str, channel: str) -> str: + return f"Profanity Filter `{status}`\nChannel: `{channel}`" + + CONFIG_CHANNEL_ID_INSTEAD_NAME = "Chnanel id should be used instead of its name!!!" CONFIG_NOT_ACTIVATED_ERROR = "Profanity Filter could not be activated.\n" MISING_REUIRED_ARGUMENT = "Missing required argument!!!" CHANNEL_ID_NOT_FOUND = "Channel id not found" -BOT_MISSING_MANAGE_MESSAGES_PERMISSION = "Bot does not have permission to \"Manage Messages\"" +BOT_MISSING_MANAGE_MESSAGES_PERMISSION = 'Bot does not have permission to "Manage Messages"' NO_CHANNELS_LISTED = "No channels listed" ################################# # EVENT CUSTOM COMMAND @@ -73,18 +98,23 @@ "Direct messages are disable in your configuration.\n" "If you want to receive messages from Bots, " "you need to enable this option under Privacy & Safety:" - "\"Allow direct messages from server members.\"" + '"Allow direct messages from server members."' ) ################################# # EVENT ON GUILD JOIN ################################# -GUILD_JOIN_BOT_MESSAGE = ( - "Thanks for using *{0}*\n" - "To learn more about this bot: `{1}about`\n" - "Games included so far: `{2}`\n\n" - "If you are an Admin and wish to list configurations: `{3}config list`\n" - "To get a list of commands: `{4}help`" -) + + +def guild_join_bot_message(bot_name: str, prefix: str, games_included: str) -> str: + return ( + f"Thanks for using *{bot_name}*\n" + f"To learn more about this bot: `{prefix}about`\n" + f"Games included so far: `{games_included}`\n\n" + f"If you are an Admin and wish to list configurations: `{prefix}config list`\n" + f"To get a list of commands: `{prefix}help`" + ) + + ################################# # EVENT ON GUILD UPDATE ################################# @@ -121,17 +151,22 @@ BOT_REACT_STUPID = "I'm not stupid, fu ufk!!!" BOT_REACT_RETARD = "I'm not retard, fu ufk!!!" MESSAGE_CENSURED = "Your message was censored.\nPlease don't say offensive words in this channel." -BLOCKED_INVIS_MESSAGE = ( - "You are Invisible (offline)\n" - "Server \"{0}\" does not allow messages from invisible members.\n" - "Please change your status if you want to send messages to this server." -) PRIVATE_BOT_MESSAGE = ( "This is a Private Bot.\n" "You are not allowed to execute any commands.\n" "Only a few users are allowed to use it.\n" "Please don't insist. Thank You!!!" ) + + +def blocked_invis_message(guild_name: str) -> str: + return ( + "You are Invisible (offline)\n" + f'Server "{guild_name}" does not allow messages from invisible members.\n' + "Please change your status if you want to send messages to this server." + ) + + ################################# # EVENT ON USER UPDATE ################################# @@ -148,7 +183,7 @@ "Direct messages are disable in your configuration.\n" "If you want to receive messages from Bots, " "you need to enable this option under Privacy & Safety:\n" - "\"Allow direct messages from server members.\"\n" + '"Allow direct messages from server members."\n' ) MESSAGE_REMOVED_FOR_PRIVACY = "Your message was removed for privacy." DELETE_MESSAGE_NO_PERMISSION = "Bot does not have permission to delete messages." @@ -162,9 +197,14 @@ MEMBER_HIGHEST_ROLL = "Your highest roll is now:" MEMBER_HAS_HIGHEST_ROLL = "has the server highest roll with" DICE_SIZE_HIGHER_ONE = "Dice size needs to be higher than 1" -NO_DICE_SIZE_ROLLS = "There are no dice rolls of the size {0} in this server." RESET_ALL_ROLLS = "Reset all rolls from this server" DELETED_ALL_ROLLS = "Rolls from all members in this server have been deleted." + + +def no_dice_size_rolls(dice_size) -> str: + return f"There are no dice rolls of the size {dice_size} in this server." + + ################################# # MISC ################################# @@ -178,10 +218,15 @@ JOINED_DISCORD_ON = "Joined Discord on" JOINED_THIS_SERVER_ON = "Joined this server on" LIST_COMMAND_CATEGORIES = "For a list of command categories" -DEV_INFO_MSG = ( - "Developed as an open source project and hosted on [GitHub]({0})\n" - "A python discord api wrapper: [discord.py]({1})\n" -) + + +def dev_info_msg(webpage_url: str, discordpy_url: str) -> str: + return ( + f"Developed as an open source project and hosted on [GitHub]({webpage_url})\n" + f"A python discord api wrapper: [discord.py]({discordpy_url})\n" + ) + + ################################# # OWNER ################################# diff --git a/src/bot/constants/variables.py b/src/bot/constants/variables.py index 88aed8cd..6b90c27d 100644 --- a/src/bot/constants/variables.py +++ b/src/bot/constants/variables.py @@ -30,6 +30,8 @@ def _get_project_version() -> str: def _discover_cogs() -> list[str]: """Discover and return all cog file paths in the correct loading order.""" + from src.gw2.cogs import discover_gw2_cogs + bot_cogs_dir = Path("src") / "bot" / "cogs" # Bot cogs - admin.py loads first for command group registration bot_cogs = [str(bot_cogs_dir / "admin" / "admin.py")] @@ -39,13 +41,7 @@ def _discover_cogs() -> list[str]: admin_cogs = [str(p) for p in (bot_cogs_dir / "admin").glob("*.py") if p.name != _INIT_PY] bot_cogs.extend(cog for cog in admin_cogs if cog not in bot_cogs) - # GW2 cogs - gw2.py loads first for command group registration - gw2_cogs_dir = Path("src") / "gw2" / "cogs" - gw2_cogs = [str(gw2_cogs_dir / "gw2.py")] - remaining_gw2 = [str(p) for p in gw2_cogs_dir.glob("*.py") if p.name != _INIT_PY] - gw2_cogs.extend(cog for cog in remaining_gw2 if cog not in gw2_cogs) - - return bot_cogs + gw2_cogs + return bot_cogs + discover_gw2_cogs() # Base directory (needed by functions above) diff --git a/src/bot/discord_bot.py b/src/bot/discord_bot.py index 298641a5..9c0a6887 100644 --- a/src/bot/discord_bot.py +++ b/src/bot/discord_bot.py @@ -53,8 +53,16 @@ def _load_settings(self) -> None: # Load bot settings from environment variables self.settings["bot"] = { "BGActivityTimer": bot_settings.bg_activity_timer, - "AllowedDMCommands": bot_settings.allowed_dm_commands, - "BotReactionWords": bot_settings.bot_reaction_words, + "AllowedDMCommands": ( + [cmd.strip() for cmd in bot_settings.allowed_dm_commands.split(",")] + if bot_settings.allowed_dm_commands + else None + ), + "BotReactionWords": ( + [word.strip() for word in bot_settings.bot_reaction_words.split(",")] + if bot_settings.bot_reaction_words + else [] + ), "EmbedColor": bot_utils.get_color_settings(bot_settings.embed_color), "EmbedOwnerColor": bot_utils.get_color_settings(bot_settings.embed_owner_color), "ExclusiveUsers": bot_settings.exclusive_users, diff --git a/src/bot/tools/background_tasks.py b/src/bot/tools/background_tasks.py index ca1bd2f8..941656d7 100644 --- a/src/bot/tools/background_tasks.py +++ b/src/bot/tools/background_tasks.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import asyncio import discord import random diff --git a/src/bot/tools/bot_utils.py b/src/bot/tools/bot_utils.py index 5574b2cd..7566066e 100644 --- a/src/bot/tools/bot_utils.py +++ b/src/bot/tools/bot_utils.py @@ -289,5 +289,5 @@ def get_bot_stats(bot: commands.Bot) -> dict[str, str | datetime]: "servers": f"{len(bot.guilds)} servers", "users": f"({unique_users} users)({bot_users} bots)[{len(bot.users)} total]", "channels": f"({text_channels} text)({voice_channels} voice)[{total_channels} total]", - "start_time": getattr(bot, 'start_time', None) or get_current_date_time(), + "start_time": getattr(bot, "start_time", None) or get_current_date_time(), } diff --git a/src/bot/tools/custom_help.py b/src/bot/tools/custom_help.py index c5ad28d9..38e29961 100644 --- a/src/bot/tools/custom_help.py +++ b/src/bot/tools/custom_help.py @@ -1,15 +1,111 @@ import discord +import itertools from discord.ext import commands +class HelpPaginatorView(discord.ui.View): + """Interactive pagination view for help pages with Previous/Next buttons.""" + + def __init__(self, pages: list[str], author_id: int): + super().__init__(timeout=300) + self.pages = pages + self.current_page = 0 + self.author_id = author_id + self.message: discord.Message | None = None + self._update_buttons() + + def _format_page(self) -> str: + page_header = f"**Page {self.current_page + 1}/{len(self.pages)}**\n" + return page_header + self.pages[self.current_page] + + def _update_buttons(self): + self.previous_button.disabled = self.current_page == 0 + self.page_indicator.label = f"{self.current_page + 1}/{len(self.pages)}" + self.next_button.disabled = self.current_page == len(self.pages) - 1 + + @discord.ui.button(label="\u25c0", style=discord.ButtonStyle.secondary) + async def previous_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id != self.author_id: + return await interaction.response.send_message( + "Only the command invoker can use these buttons.", ephemeral=True + ) + self.current_page -= 1 + self._update_buttons() + await interaction.response.edit_message(content=self._format_page(), view=self) + + @discord.ui.button(label="1/1", style=discord.ButtonStyle.secondary, disabled=True) + async def page_indicator(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer() + + @discord.ui.button(label="\u25b6", style=discord.ButtonStyle.secondary) + async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id != self.author_id: + return await interaction.response.send_message( + "Only the command invoker can use these buttons.", ephemeral=True + ) + self.current_page += 1 + self._update_buttons() + await interaction.response.edit_message(content=self._format_page(), view=self) + + async def on_timeout(self): + for item in self.children: + item.disabled = True + try: + if self.message: + await self.message.edit(view=self) + except discord.NotFound, discord.HTTPException: + pass + + class CustomHelpCommand(commands.DefaultHelpCommand): """Custom help command that sends DM notifications to the channel.""" def __init__(self, **options): # Increase page size for bigger window - options.setdefault('paginator', commands.Paginator(prefix='```', suffix='```', max_size=2000)) + options.setdefault("paginator", commands.Paginator(prefix="```", suffix="```", max_size=2000)) super().__init__(**options) + async def send_bot_help(self, mapping): + """Override to add per-group subcommand pages after the overview page.""" + ctx = self.context + bot = ctx.bot + + if bot.description: + self.paginator.add_line(bot.description, empty=True) + + no_category = f"\u200b{self.no_category}:" + + def get_category(command, *, _no_category=no_category): + cog = command.cog + return f"{cog.qualified_name}:" if cog is not None else _no_category + + filtered = await self.filter_commands(bot.commands, sort=True, key=get_category) + max_size = self.get_max_size(filtered) + to_iterate = itertools.groupby(filtered, key=get_category) + + groups = [] + for category, cmds in to_iterate: + cmds = sorted(cmds, key=lambda c: c.name) if self.sort_commands else list(cmds) + self.add_indented_commands(cmds, heading=category, max_size=max_size) + for cmd in cmds: + if isinstance(cmd, commands.Group): + groups.append(cmd) + + note = self.get_ending_note() + if note: + self.paginator.add_line() + self.paginator.add_line(note) + + # Add a separate page for each group with its subcommands + for group in groups: + subcommands = await self.filter_commands(group.commands, sort=self.sort_commands) + if subcommands: + self.paginator.close_page() + heading = f"{group.qualified_name} subcommands:" + self.add_indented_commands(subcommands, heading=heading, max_size=self.get_max_size(subcommands)) + + await self.send_pages() + async def send_pages(self): """Override send_pages to send as paginated regular messages.""" destination = self.get_destination() @@ -38,23 +134,21 @@ async def send_pages(self): await self._send_pages_to_destination(destination) async def _send_pages_to_dm(self): - """Send paginated messages to user's DM.""" - for page_num, page in enumerate(self.paginator.pages, 1): - if len(self.paginator.pages) > 1: - # Add page numbers for multiple pages - page_header = f"**Page {page_num}/{len(self.paginator.pages)}**\n" - content = page_header + page - else: - content = page - await self.context.author.send(content) + """Send help pages to user's DM with button pagination.""" + pages = self.paginator.pages + if len(pages) == 1: + await self.context.author.send(pages[0]) + else: + view = HelpPaginatorView(pages, self.context.author.id) + msg = await self.context.author.send(content=view._format_page(), view=view) + view.message = msg async def _send_pages_to_destination(self, destination): - """Send paginated messages to specified destination.""" - for page_num, page in enumerate(self.paginator.pages, 1): - if len(self.paginator.pages) > 1: - # Add page numbers for multiple pages - page_header = f"**Page {page_num}/{len(self.paginator.pages)}**\n" - content = page_header + page - else: - content = page - await destination.send(content) + """Send help pages to specified destination with button pagination.""" + pages = self.paginator.pages + if len(pages) == 1: + await destination.send(pages[0]) + else: + view = HelpPaginatorView(pages, self.context.author.id) + msg = await destination.send(content=view._format_page(), view=view) + view.message = msg diff --git a/src/bot/tools/pepe.py b/src/bot/tools/pepe.py index 8ccbc642..b5030b2c 100644 --- a/src/bot/tools/pepe.py +++ b/src/bot/tools/pepe.py @@ -1,102 +1,102 @@ pepedatabase = [ - 'https://i.imgur.com/klkeMme.png', - 'https://i.imgur.com/yMz6s30.png', - 'https://i.imgur.com/c7OUZyV.png', - 'https://i.imgur.com/85tffg2.png', - 'https://i.imgur.com/DLDe5gG.png', - 'https://i.imgur.com/EbOxDzB.png', - 'https://i.imgur.com/wxEW6Qn.png', - 'https://i.imgur.com/8KMtiGC.png', - 'https://i.imgur.com/iRUd8rI.png', - 'https://i.imgur.com/1pR1FiB.png', - 'https://i.imgur.com/zXno1OF.png', - 'https://i.imgur.com/RJPUjAx.png', - 'https://i.imgur.com/oBbgpk7.png', - 'https://i.imgur.com/XUA5p5k.png', - 'https://i.imgur.com/P5GYYWJ.png', - 'https://i.imgur.com/RW3hkvw.png', - 'https://i.imgur.com/vMc0wjk.png', - 'https://i.imgur.com/a02IV0p.png', - 'https://i.imgur.com/C9HcBU3.png', - 'https://i.imgur.com/LIYDk27.png', - 'https://i.imgur.com/Yf8Pcsz.png', - 'https://i.imgur.com/VIYwB7E.png', - 'https://i.imgur.com/t1rqzy9.png', - 'https://i.imgur.com/QriJh6a.png', - 'https://i.imgur.com/mpj7TAW.png', - 'https://i.imgur.com/3unhsqY.png', - 'https://i.imgur.com/L0sj23T.png', - 'https://i.imgur.com/aDW7N0x.png', - 'https://i.imgur.com/WwFtyWv.png', - 'https://i.imgur.com/8dOwi9q.png', - 'https://i.imgur.com/EL7hmKz.png', - 'https://i.imgur.com/VoqKhML.png', - 'https://i.imgur.com/Ry7wYAC.png', - 'https://i.imgur.com/7rSPEnc.png', - 'https://i.imgur.com/tSrV0Kk.png', - 'https://i.imgur.com/YMo3FCC.png', - 'https://i.imgur.com/QqwYHFm.png', - 'https://i.imgur.com/ICRqk4t.png', - 'https://i.imgur.com/1lnY9Ec.png', - 'https://i.imgur.com/JSdMtKo.png', - 'https://i.imgur.com/pF7DpfZ.png', - 'https://i.imgur.com/gyU3RvV.png', - 'https://i.imgur.com/I3LeJ3H.png', - 'https://i.imgur.com/7DM4y2x.png', - 'https://i.imgur.com/nSMLDtw.png', - 'https://i.imgur.com/jLpcAiV.png', - 'https://i.imgur.com/6dDQVVj.png', - 'https://i.imgur.com/6lbFsHn.png', - 'https://i.imgur.com/gSk1JHS.png', - 'https://i.imgur.com/8Vl2j66.png', - 'https://i.imgur.com/ZgNIkzg.png', - 'https://i.imgur.com/9kSRLBK.png', - 'https://i.imgur.com/186etyr.png', - 'https://i.imgur.com/z6X0ly7.png', - 'https://i.imgur.com/4xzpie7.png', - 'https://i.imgur.com/EGw6Xz7.png', - 'https://i.imgur.com/Dk7M23N.png', - 'https://i.imgur.com/HvK6DGX.png', - 'https://i.imgur.com/F5Si2qo.png', - 'https://i.imgur.com/8ytbWYt.png', - 'https://i.imgur.com/vsz5yKk.png', - 'https://i.imgur.com/HDK0Xw2.png', - 'https://i.imgur.com/tlvEJkM.png', - 'https://i.imgur.com/oqPTXoj.png', - 'https://i.imgur.com/aXQaVW4.png', - 'https://i.imgur.com/fepY07Z.png', - 'https://i.imgur.com/WLG760e.png', - 'https://i.imgur.com/1fnolXU.png', - 'https://i.imgur.com/822fa0N.png', - 'https://i.imgur.com/Z8BHwCY.png', - 'https://i.imgur.com/4efGIER.png', - 'https://i.imgur.com/gvLWCIX.png', - 'https://i.imgur.com/IpIcL7q.png', - 'https://i.imgur.com/VSlc5Gv.png', - 'https://i.imgur.com/hpmARP7.png', - 'https://i.imgur.com/XBLQdEG.png', - 'https://i.imgur.com/nSs2AhR.png', - 'https://i.imgur.com/Pce6wWI.png', - 'https://i.imgur.com/bb1J7bu.png', - 'https://i.imgur.com/75hinzs.png', - 'https://i.imgur.com/UYrpJsm.png', - 'https://i.imgur.com/d5Bo0o3.png', - 'https://i.imgur.com/Tbd1nKx.png', - 'https://i.imgur.com/pBki9NQ.png', - 'https://i.imgur.com/3PfY6vY.png', - 'https://i.imgur.com/mibPYHe.png', - 'https://i.imgur.com/GEPqZnV.png', - 'https://i.imgur.com/Acm01Xv.png', - 'https://i.imgur.com/K0jnnSg.png', - 'https://i.imgur.com/HAHvjzM.png', - 'https://i.imgur.com/T8XerLn.png', - 'https://i.imgur.com/sn74Yt5.png', - 'https://i.imgur.com/LqLvjEC.png', - 'https://i.imgur.com/vORym8X.png', - 'https://i.imgur.com/HJLjrdk.png', - 'https://i.imgur.com/StllvTP.png', - 'https://i.imgur.com/P8MDXEw.png', - 'https://i.imgur.com/cW9NrvO.png', - 'https://i.imgur.com/XbZa2lr.png', - 'https://i.imgur.com/UaBpIId.png', + "https://i.imgur.com/klkeMme.png", + "https://i.imgur.com/yMz6s30.png", + "https://i.imgur.com/c7OUZyV.png", + "https://i.imgur.com/85tffg2.png", + "https://i.imgur.com/DLDe5gG.png", + "https://i.imgur.com/EbOxDzB.png", + "https://i.imgur.com/wxEW6Qn.png", + "https://i.imgur.com/8KMtiGC.png", + "https://i.imgur.com/iRUd8rI.png", + "https://i.imgur.com/1pR1FiB.png", + "https://i.imgur.com/zXno1OF.png", + "https://i.imgur.com/RJPUjAx.png", + "https://i.imgur.com/oBbgpk7.png", + "https://i.imgur.com/XUA5p5k.png", + "https://i.imgur.com/P5GYYWJ.png", + "https://i.imgur.com/RW3hkvw.png", + "https://i.imgur.com/vMc0wjk.png", + "https://i.imgur.com/a02IV0p.png", + "https://i.imgur.com/C9HcBU3.png", + "https://i.imgur.com/LIYDk27.png", + "https://i.imgur.com/Yf8Pcsz.png", + "https://i.imgur.com/VIYwB7E.png", + "https://i.imgur.com/t1rqzy9.png", + "https://i.imgur.com/QriJh6a.png", + "https://i.imgur.com/mpj7TAW.png", + "https://i.imgur.com/3unhsqY.png", + "https://i.imgur.com/L0sj23T.png", + "https://i.imgur.com/aDW7N0x.png", + "https://i.imgur.com/WwFtyWv.png", + "https://i.imgur.com/8dOwi9q.png", + "https://i.imgur.com/EL7hmKz.png", + "https://i.imgur.com/VoqKhML.png", + "https://i.imgur.com/Ry7wYAC.png", + "https://i.imgur.com/7rSPEnc.png", + "https://i.imgur.com/tSrV0Kk.png", + "https://i.imgur.com/YMo3FCC.png", + "https://i.imgur.com/QqwYHFm.png", + "https://i.imgur.com/ICRqk4t.png", + "https://i.imgur.com/1lnY9Ec.png", + "https://i.imgur.com/JSdMtKo.png", + "https://i.imgur.com/pF7DpfZ.png", + "https://i.imgur.com/gyU3RvV.png", + "https://i.imgur.com/I3LeJ3H.png", + "https://i.imgur.com/7DM4y2x.png", + "https://i.imgur.com/nSMLDtw.png", + "https://i.imgur.com/jLpcAiV.png", + "https://i.imgur.com/6dDQVVj.png", + "https://i.imgur.com/6lbFsHn.png", + "https://i.imgur.com/gSk1JHS.png", + "https://i.imgur.com/8Vl2j66.png", + "https://i.imgur.com/ZgNIkzg.png", + "https://i.imgur.com/9kSRLBK.png", + "https://i.imgur.com/186etyr.png", + "https://i.imgur.com/z6X0ly7.png", + "https://i.imgur.com/4xzpie7.png", + "https://i.imgur.com/EGw6Xz7.png", + "https://i.imgur.com/Dk7M23N.png", + "https://i.imgur.com/HvK6DGX.png", + "https://i.imgur.com/F5Si2qo.png", + "https://i.imgur.com/8ytbWYt.png", + "https://i.imgur.com/vsz5yKk.png", + "https://i.imgur.com/HDK0Xw2.png", + "https://i.imgur.com/tlvEJkM.png", + "https://i.imgur.com/oqPTXoj.png", + "https://i.imgur.com/aXQaVW4.png", + "https://i.imgur.com/fepY07Z.png", + "https://i.imgur.com/WLG760e.png", + "https://i.imgur.com/1fnolXU.png", + "https://i.imgur.com/822fa0N.png", + "https://i.imgur.com/Z8BHwCY.png", + "https://i.imgur.com/4efGIER.png", + "https://i.imgur.com/gvLWCIX.png", + "https://i.imgur.com/IpIcL7q.png", + "https://i.imgur.com/VSlc5Gv.png", + "https://i.imgur.com/hpmARP7.png", + "https://i.imgur.com/XBLQdEG.png", + "https://i.imgur.com/nSs2AhR.png", + "https://i.imgur.com/Pce6wWI.png", + "https://i.imgur.com/bb1J7bu.png", + "https://i.imgur.com/75hinzs.png", + "https://i.imgur.com/UYrpJsm.png", + "https://i.imgur.com/d5Bo0o3.png", + "https://i.imgur.com/Tbd1nKx.png", + "https://i.imgur.com/pBki9NQ.png", + "https://i.imgur.com/3PfY6vY.png", + "https://i.imgur.com/mibPYHe.png", + "https://i.imgur.com/GEPqZnV.png", + "https://i.imgur.com/Acm01Xv.png", + "https://i.imgur.com/K0jnnSg.png", + "https://i.imgur.com/HAHvjzM.png", + "https://i.imgur.com/T8XerLn.png", + "https://i.imgur.com/sn74Yt5.png", + "https://i.imgur.com/LqLvjEC.png", + "https://i.imgur.com/vORym8X.png", + "https://i.imgur.com/HJLjrdk.png", + "https://i.imgur.com/StllvTP.png", + "https://i.imgur.com/P8MDXEw.png", + "https://i.imgur.com/cW9NrvO.png", + "https://i.imgur.com/XbZa2lr.png", + "https://i.imgur.com/UaBpIId.png", ] diff --git a/src/database/dal/bot/servers_dal.py b/src/database/dal/bot/servers_dal.py index 361f8396..20665ad6 100644 --- a/src/database/dal/bot/servers_dal.py +++ b/src/database/dal/bot/servers_dal.py @@ -18,7 +18,7 @@ async def insert_server(self, server_id: int, name: str): name=name, ) # On conflict, update the name in case it changed - stmt = stmt.on_conflict_do_update(index_elements=['id'], set_={'name': name}) + stmt = stmt.on_conflict_do_update(index_elements=["id"], set_={"name": name}) await self.db_utils.execute(stmt) async def update_server(self, before: discord.Guild, after: discord.Guild): diff --git a/src/database/dal/gw2/gw2_session_chars_dal.py b/src/database/dal/gw2/gw2_session_chars_dal.py index 71bc877a..c1e7e02b 100644 --- a/src/database/dal/gw2/gw2_session_chars_dal.py +++ b/src/database/dal/gw2/gw2_session_chars_dal.py @@ -23,15 +23,17 @@ async def insert_session_char(self, gw2_api, api_characters, insert_args: dict): name=name, profession=profession, deaths=deaths, + start=insert_args["start"], + end=insert_args["end"], ) await self.db_utils.insert(stmt) async def get_all_start_characters(self, user_id: int): - stmt = select(*self.columns).where(Gw2SessionChars.user_id == user_id, Gw2SessionChars.start is True) + stmt = select(*self.columns).where(Gw2SessionChars.user_id == user_id, Gw2SessionChars.start.is_(True)) results = await self.db_utils.fetchall(stmt, True) return results async def get_all_end_characters(self, user_id: int): - stmt = select(*self.columns).where(Gw2SessionChars.user_id == user_id, Gw2SessionChars.end is True) + stmt = select(*self.columns).where(Gw2SessionChars.user_id == user_id, Gw2SessionChars.end.is_(True)) results = await self.db_utils.fetchall(stmt, True) return results diff --git a/src/database/migrations/versions/0001_create_functions.py b/src/database/migrations/versions/0001_create_functions.py index 2b126785..4f2b7dcb 100644 --- a/src/database/migrations/versions/0001_create_functions.py +++ b/src/database/migrations/versions/0001_create_functions.py @@ -10,7 +10,7 @@ from collections.abc import Sequence from ddcDatabases.postgresql import get_postgresql_settings -revision: str = '0001' +revision: str = "0001" down_revision: str | None = None branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None diff --git a/src/database/migrations/versions/0002_bot_configs.py b/src/database/migrations/versions/0002_bot_configs.py index 763efb4a..157ce2e4 100644 --- a/src/database/migrations/versions/0002_bot_configs.py +++ b/src/database/migrations/versions/0002_bot_configs.py @@ -13,8 +13,8 @@ from src.database.models.bot_models import BotConfigs # revision identifiers, used by Alembic. -revision: str = '0002' -down_revision: str | None = '0001' +revision: str = "0002" +down_revision: str | None = "0001" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -22,16 +22,16 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - 'bot_configs', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('prefix', sa.CHAR(length=1), server_default=variables.PREFIX, nullable=False), - sa.Column('author_id', sa.BigInteger(), server_default=variables.AUTHOR_ID, nullable=False), - sa.Column('url', sa.String(), server_default=variables.BOT_WEBPAGE_URL, nullable=False), - sa.Column('description', sa.String(), server_default=variables.DESCRIPTION, nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id'), + "bot_configs", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("prefix", sa.CHAR(length=1), server_default=variables.PREFIX, nullable=False), + sa.Column("author_id", sa.BigInteger(), server_default=variables.AUTHOR_ID, nullable=False), + sa.Column("url", sa.String(), server_default=variables.BOT_WEBPAGE_URL, nullable=False), + sa.Column("description", sa.String(), server_default=variables.DESCRIPTION, nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), ) op.execute( sa.insert(BotConfigs).values( @@ -53,6 +53,6 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.execute('DROP TRIGGER IF EXISTS before_update_bot_configs_tr ON bot_configs') - op.drop_table('bot_configs') + op.execute("DROP TRIGGER IF EXISTS before_update_bot_configs_tr ON bot_configs") + op.drop_table("bot_configs") # ### end Alembic commands ### diff --git a/src/database/migrations/versions/0003_servers.py b/src/database/migrations/versions/0003_servers.py index 0f848332..d98855ec 100644 --- a/src/database/migrations/versions/0003_servers.py +++ b/src/database/migrations/versions/0003_servers.py @@ -11,8 +11,8 @@ from collections.abc import Sequence # revision identifiers, used by Alembic. -revision: str = '0003' -down_revision: str | None = '0002' +revision: str = "0003" +down_revision: str | None = "0002" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -20,22 +20,22 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - 'servers', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('name', sa.String(), nullable=True), - sa.Column('msg_on_join', sa.Boolean(), server_default='1', nullable=False), - sa.Column('msg_on_leave', sa.Boolean(), server_default='1', nullable=False), - sa.Column('msg_on_server_update', sa.Boolean(), server_default='1', nullable=False), - sa.Column('msg_on_member_update', sa.Boolean(), server_default='1', nullable=False), - sa.Column('block_invis_members', sa.Boolean(), server_default='0', nullable=False), - sa.Column('bot_word_reactions', sa.Boolean(), server_default='1', nullable=False), - sa.Column('updated_by', sa.BigInteger(), nullable=True), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id'), + "servers", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("msg_on_join", sa.Boolean(), server_default="1", nullable=False), + sa.Column("msg_on_leave", sa.Boolean(), server_default="1", nullable=False), + sa.Column("msg_on_server_update", sa.Boolean(), server_default="1", nullable=False), + sa.Column("msg_on_member_update", sa.Boolean(), server_default="1", nullable=False), + sa.Column("block_invis_members", sa.Boolean(), server_default="0", nullable=False), + sa.Column("bot_word_reactions", sa.Boolean(), server_default="1", nullable=False), + sa.Column("updated_by", sa.BigInteger(), nullable=True), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), ) - op.create_index(op.f('ix_servers_id'), 'servers', ['id'], unique=True) + op.create_index(op.f("ix_servers_id"), "servers", ["id"], unique=True) op.execute(""" CREATE TRIGGER before_update_servers_tr BEFORE UPDATE ON servers @@ -47,7 +47,7 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.execute('DROP TRIGGER IF EXISTS before_update_servers_tr ON servers') - op.drop_index(op.f('ix_servers_id'), table_name='servers') - op.drop_table('servers') + op.execute("DROP TRIGGER IF EXISTS before_update_servers_tr ON servers") + op.drop_index(op.f("ix_servers_id"), table_name="servers") + op.drop_table("servers") # ### end Alembic commands ### diff --git a/src/database/migrations/versions/0004_custom_commands.py b/src/database/migrations/versions/0004_custom_commands.py index dcc4076e..11d59658 100644 --- a/src/database/migrations/versions/0004_custom_commands.py +++ b/src/database/migrations/versions/0004_custom_commands.py @@ -11,8 +11,8 @@ from collections.abc import Sequence # revision identifiers, used by Alembic. -revision: str = '0004' -down_revision: str | None = '0003' +revision: str = "0004" +down_revision: str | None = "0003" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -20,20 +20,20 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - 'custom_commands', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('server_id', sa.BigInteger(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('description', sa.String(), nullable=False), - sa.Column('created_by', sa.BigInteger(), nullable=True), - sa.Column('updated_by', sa.BigInteger(), nullable=True), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.ForeignKeyConstraint(['server_id'], ['servers.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id'), + "custom_commands", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("server_id", sa.BigInteger(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column("created_by", sa.BigInteger(), nullable=True), + sa.Column("updated_by", sa.BigInteger(), nullable=True), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.ForeignKeyConstraint(["server_id"], ["servers.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), ) - op.create_index(op.f('ix_custom_commands_server_id'), 'custom_commands', ['server_id'], unique=False) + op.create_index(op.f("ix_custom_commands_server_id"), "custom_commands", ["server_id"], unique=False) op.execute(""" CREATE TRIGGER before_update_custom_commands_tr BEFORE UPDATE ON custom_commands @@ -45,7 +45,7 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.execute('DROP TRIGGER IF EXISTS before_update_custom_commands_tr ON custom_commands') - op.drop_index(op.f('ix_custom_commands_server_id'), table_name='custom_commands') - op.drop_table('custom_commands') + op.execute("DROP TRIGGER IF EXISTS before_update_custom_commands_tr ON custom_commands") + op.drop_index(op.f("ix_custom_commands_server_id"), table_name="custom_commands") + op.drop_table("custom_commands") # ### end Alembic commands ### diff --git a/src/database/migrations/versions/0005_profanity_filters.py b/src/database/migrations/versions/0005_profanity_filters.py index 09514956..cf6ad0cd 100644 --- a/src/database/migrations/versions/0005_profanity_filters.py +++ b/src/database/migrations/versions/0005_profanity_filters.py @@ -11,8 +11,8 @@ from collections.abc import Sequence # revision identifiers, used by Alembic. -revision: str = '0005' -down_revision: str | None = '0004' +revision: str = "0005" +down_revision: str | None = "0004" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -20,19 +20,19 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - 'profanity_filters', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('server_id', sa.BigInteger(), nullable=False), - sa.Column('channel_id', sa.BigInteger(), nullable=False), - sa.Column('channel_name', sa.String(), nullable=False), - sa.Column('created_by', sa.BigInteger(), nullable=True), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.ForeignKeyConstraint(['server_id'], ['servers.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id'), + "profanity_filters", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("server_id", sa.BigInteger(), nullable=False), + sa.Column("channel_id", sa.BigInteger(), nullable=False), + sa.Column("channel_name", sa.String(), nullable=False), + sa.Column("created_by", sa.BigInteger(), nullable=True), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.ForeignKeyConstraint(["server_id"], ["servers.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), ) - op.create_index(op.f('ix_profanity_filters_server_id'), 'profanity_filters', ['server_id'], unique=False) + op.create_index(op.f("ix_profanity_filters_server_id"), "profanity_filters", ["server_id"], unique=False) op.execute(""" CREATE TRIGGER before_update_profanity_filters_tr BEFORE UPDATE ON profanity_filters @@ -44,7 +44,7 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.execute('DROP TRIGGER IF EXISTS before_update_profanity_filters_tr ON profanity_filters') - op.drop_index(op.f('ix_profanity_filters_server_id'), table_name='profanity_filters') - op.drop_table('profanity_filters') + op.execute("DROP TRIGGER IF EXISTS before_update_profanity_filters_tr ON profanity_filters") + op.drop_index(op.f("ix_profanity_filters_server_id"), table_name="profanity_filters") + op.drop_table("profanity_filters") # ### end Alembic commands ### diff --git a/src/database/migrations/versions/0006_dice_rolls.py b/src/database/migrations/versions/0006_dice_rolls.py index b9e2f30f..96d69dc1 100644 --- a/src/database/migrations/versions/0006_dice_rolls.py +++ b/src/database/migrations/versions/0006_dice_rolls.py @@ -11,8 +11,8 @@ from collections.abc import Sequence # revision identifiers, used by Alembic. -revision: str = '0006' -down_revision: str | None = '0005' +revision: str = "0006" +down_revision: str | None = "0005" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -20,20 +20,20 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - 'dice_rolls', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('server_id', sa.BigInteger(), nullable=False), - sa.Column('user_id', sa.BigInteger(), nullable=False), - sa.Column('roll', sa.Integer(), nullable=False), - sa.Column('dice_size', sa.Integer(), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.ForeignKeyConstraint(['server_id'], ['servers.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id'), + "dice_rolls", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("server_id", sa.BigInteger(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=False), + sa.Column("roll", sa.Integer(), nullable=False), + sa.Column("dice_size", sa.Integer(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.ForeignKeyConstraint(["server_id"], ["servers.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), ) - op.create_index(op.f('ix_dice_rolls_server_id'), 'dice_rolls', ['server_id'], unique=False) - op.create_index(op.f('ix_dice_rolls_user_id'), 'dice_rolls', ['user_id'], unique=False) + op.create_index(op.f("ix_dice_rolls_server_id"), "dice_rolls", ["server_id"], unique=False) + op.create_index(op.f("ix_dice_rolls_user_id"), "dice_rolls", ["user_id"], unique=False) op.execute(""" CREATE TRIGGER before_update_dice_rolls_tr BEFORE UPDATE ON dice_rolls @@ -45,8 +45,8 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.execute('DROP TRIGGER IF EXISTS before_update_dice_rolls_tr ON dice_rolls') - op.drop_index(op.f('ix_dice_rolls_user_id'), table_name='dice_rolls') - op.drop_index(op.f('ix_dice_rolls_server_id'), table_name='dice_rolls') - op.drop_table('dice_rolls') + op.execute("DROP TRIGGER IF EXISTS before_update_dice_rolls_tr ON dice_rolls") + op.drop_index(op.f("ix_dice_rolls_user_id"), table_name="dice_rolls") + op.drop_index(op.f("ix_dice_rolls_server_id"), table_name="dice_rolls") + op.drop_table("dice_rolls") # ### end Alembic commands ### diff --git a/src/database/migrations/versions/0007_gw2_keys.py b/src/database/migrations/versions/0007_gw2_keys.py index 4b7b041e..054d20bb 100644 --- a/src/database/migrations/versions/0007_gw2_keys.py +++ b/src/database/migrations/versions/0007_gw2_keys.py @@ -11,8 +11,8 @@ from collections.abc import Sequence # revision identifiers, used by Alembic. -revision: str = '0007' -down_revision: str | None = '0006' +revision: str = "0007" +down_revision: str | None = "0006" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -20,19 +20,19 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - 'gw2_keys', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.BigInteger(), nullable=False), - sa.Column('name', sa.String(), nullable=True), - sa.Column('gw2_acc_name', sa.String(), nullable=False), - sa.Column('server', sa.String(), nullable=False), - sa.Column('permissions', sa.String(), nullable=False), - sa.Column('key', sa.String(), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id'), - sa.UniqueConstraint('user_id'), + "gw2_keys", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("gw2_acc_name", sa.String(), nullable=False), + sa.Column("server", sa.String(), nullable=False), + sa.Column("permissions", sa.String(), nullable=False), + sa.Column("key", sa.String(), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + sa.UniqueConstraint("user_id"), ) op.execute(""" CREATE TRIGGER before_update_gw2_keys_tr @@ -45,6 +45,6 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.execute('DROP TRIGGER IF EXISTS before_update_gw2_keys_tr ON gw2_keys') - op.drop_table('gw2_keys') + op.execute("DROP TRIGGER IF EXISTS before_update_gw2_keys_tr ON gw2_keys") + op.drop_table("gw2_keys") # ### end Alembic commands ### diff --git a/src/database/migrations/versions/0008_gw2_configs.py b/src/database/migrations/versions/0008_gw2_configs.py index 8e96cd16..770e0387 100644 --- a/src/database/migrations/versions/0008_gw2_configs.py +++ b/src/database/migrations/versions/0008_gw2_configs.py @@ -11,8 +11,8 @@ from collections.abc import Sequence # revision identifiers, used by Alembic. -revision: str = '0008' -down_revision: str | None = '0007' +revision: str = "0008" +down_revision: str | None = "0007" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -20,17 +20,17 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - 'gw2_configs', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('server_id', sa.BigInteger(), nullable=False), - sa.Column('session', sa.Boolean(), server_default='0', nullable=False), - sa.Column('updated_by', sa.BigInteger(), nullable=True), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.ForeignKeyConstraint(['server_id'], ['servers.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id'), - sa.UniqueConstraint('server_id'), + "gw2_configs", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("server_id", sa.BigInteger(), nullable=False), + sa.Column("session", sa.Boolean(), server_default="0", nullable=False), + sa.Column("updated_by", sa.BigInteger(), nullable=True), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.ForeignKeyConstraint(["server_id"], ["servers.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + sa.UniqueConstraint("server_id"), ) op.execute(""" CREATE TRIGGER before_update_gw2_configs_tr @@ -43,6 +43,6 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.execute('DROP TRIGGER IF EXISTS before_update_gw2_configs_tr ON gw2_configs') - op.drop_table('gw2_configs') + op.execute("DROP TRIGGER IF EXISTS before_update_gw2_configs_tr ON gw2_configs") + op.drop_table("gw2_configs") # ### end Alembic commands ### diff --git a/src/database/migrations/versions/0009_gw2_sessions.py b/src/database/migrations/versions/0009_gw2_sessions.py index 7d63ff5f..0536d941 100644 --- a/src/database/migrations/versions/0009_gw2_sessions.py +++ b/src/database/migrations/versions/0009_gw2_sessions.py @@ -12,8 +12,8 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision: str = '0009' -down_revision: str | None = '0008' +revision: str = "0009" +down_revision: str | None = "0008" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -21,16 +21,16 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - 'gw2_sessions', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.BigInteger(), nullable=False), - sa.Column('acc_name', sa.String(), nullable=False), - sa.Column('start', postgresql.JSONB(astext_type=sa.Text()), nullable=False), - sa.Column('end', postgresql.JSONB(astext_type=sa.Text()), nullable=True), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id'), + "gw2_sessions", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=False), + sa.Column("acc_name", sa.String(), nullable=False), + sa.Column("start", postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.Column("end", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), ) op.execute(""" CREATE TRIGGER before_update_gw2_sessions_tr @@ -43,6 +43,6 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.execute('DROP TRIGGER IF EXISTS before_update_gw2_sessions_tr ON gw2_sessions') - op.drop_table('gw2_sessions') + op.execute("DROP TRIGGER IF EXISTS before_update_gw2_sessions_tr ON gw2_sessions") + op.drop_table("gw2_sessions") # ### end Alembic commands ### diff --git a/src/database/migrations/versions/0010_gw2_session_chars.py b/src/database/migrations/versions/0010_gw2_session_chars.py index 79a0896d..f8ce8d55 100644 --- a/src/database/migrations/versions/0010_gw2_session_chars.py +++ b/src/database/migrations/versions/0010_gw2_session_chars.py @@ -11,8 +11,8 @@ from collections.abc import Sequence # revision identifiers, used by Alembic. -revision: str = '0010' -down_revision: str | None = '0009' +revision: str = "0010" +down_revision: str | None = "0009" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -20,24 +20,24 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( - 'gw2_session_chars', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('session_id', sa.BigInteger(), nullable=False), - sa.Column('user_id', sa.BigInteger(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('profession', sa.String(), nullable=False), - sa.Column('deaths', sa.Integer(), nullable=False), - sa.Column('start', sa.Boolean(), nullable=False), - sa.Column('end', sa.Boolean(), nullable=True), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), - sa.Column('created_at', sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + "gw2_session_chars", + sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column("session_id", sa.BigInteger(), nullable=False), + sa.Column("user_id", sa.BigInteger(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("profession", sa.String(), nullable=False), + sa.Column("deaths", sa.Integer(), nullable=False), + sa.Column("start", sa.Boolean(), nullable=False), + sa.Column("end", sa.Boolean(), nullable=True), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("(now() at time zone 'utc')"), nullable=False), sa.ForeignKeyConstraint( - ['session_id'], - ['gw2_sessions.id'], + ["session_id"], + ["gw2_sessions.id"], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id'), - sa.UniqueConstraint('name'), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id"), + sa.UniqueConstraint("name"), ) op.execute(""" CREATE TRIGGER before_update_gw2_session_chars_tr @@ -50,6 +50,6 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.execute('DROP TRIGGER IF EXISTS before_update_gw2_session_chars_tr ON gw2_session_chars') - op.drop_table('gw2_session_chars') + op.execute("DROP TRIGGER IF EXISTS before_update_gw2_session_chars_tr ON gw2_session_chars") + op.drop_table("gw2_session_chars") # ### end Alembic commands ### diff --git a/src/gw2/cogs/__init__.py b/src/gw2/cogs/__init__.py index e69de29b..a561c9e2 100644 --- a/src/gw2/cogs/__init__.py +++ b/src/gw2/cogs/__init__.py @@ -0,0 +1,10 @@ +from pathlib import Path + + +def discover_gw2_cogs() -> list[str]: + """Discover GW2 cog file paths with gw2.py first for command group registration.""" + cogs_dir = Path("src") / "gw2" / "cogs" + cogs = [str(cogs_dir / "gw2.py")] + remaining = [str(p) for p in cogs_dir.glob("*.py") if p.name != "__init__.py"] + cogs.extend(c for c in remaining if c not in cogs) + return cogs diff --git a/src/gw2/cogs/account.py b/src/gw2/cogs/account.py index 02c47ec1..ac3f97f7 100644 --- a/src/gw2/cogs/account.py +++ b/src/gw2/cogs/account.py @@ -5,6 +5,7 @@ from src.database.dal.gw2.gw2_key_dal import Gw2KeyDal from src.gw2.cogs.gw2 import GuildWars2 from src.gw2.constants import gw2_messages +from src.gw2.constants.gw2_teams import get_team_name, is_wr_team_id from src.gw2.tools import gw2_utils from src.gw2.tools.gw2_client import Gw2Client from src.gw2.tools.gw2_cooldowns import GW2CoolDowns @@ -40,7 +41,7 @@ async def _fetch_guild_info_standalone(gw2_api, guild_id, api_key, ctx): class GW2Account(GuildWars2): - """(Commands related to users account)""" + """Guild Wars 2 commands for account information.""" def __init__(self, bot): super().__init__(bot) @@ -49,8 +50,11 @@ def __init__(self, bot): @GW2Account.gw2.command() @commands.cooldown(1, GW2CoolDowns.Account.seconds, commands.BucketType.user) async def account(ctx): - """(General information about your GW2 account) + """Display general information about your Guild Wars 2 account. + Required API permissions: account + + Usage: gw2 account """ @@ -59,8 +63,8 @@ async def account(ctx): rs = await gw2_key_dal.get_api_key_by_user(ctx.message.author.id) if not rs: msg = gw2_messages.NO_API_KEY - msg += gw2_messages.KEY_ADD_INFO_HELP.format(ctx.prefix) - msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) + msg += gw2_messages.key_add_info_help(ctx.prefix) + msg += gw2_messages.key_more_info_help(ctx.prefix) return await bot_utils.send_error_msg(ctx, msg) api_key = str(rs[0]["key"]) @@ -71,9 +75,9 @@ async def account(ctx): is_valid_key = await gw2_api.check_api_key(api_key) if not isinstance(is_valid_key, dict): msg = f"{is_valid_key.args[1]}\n" - msg += gw2_messages.INVALID_API_KEY_HELP_MESSAGE.format(ctx.prefix) - msg += gw2_messages.KEY_ADD_INFO_HELP.format(ctx.prefix) - msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) + msg += gw2_messages.INVALID_API_KEY_HELP_MESSAGE + msg += gw2_messages.key_add_info_help(ctx.prefix) + msg += gw2_messages.key_more_info_help(ctx.prefix) return await bot_utils.send_error_msg(ctx, msg) if "account" not in permissions: @@ -104,23 +108,24 @@ async def account(ctx): api_req_acc = await account_task server_id = api_req_acc["world"] - # Now fetch server info - server_task = gw2_api.call_api(f"worlds/{server_id}", api_key) - # Prepare basic account data acc_name = api_req_acc["name"] access_normalized = [] for each in api_req_acc["access"]: normalized = "".join([f" {c.upper()}" if c.isupper() or c.isdigit() else c for c in each]).lstrip() access_normalized.append(normalized) - access = '\n'.join(access_normalized) + access = "\n".join(access_normalized) is_commander = "Yes" if api_req_acc["commander"] else "No" - # Get server info - api_req_server = await server_task - server_name = api_req_server["name"] - population = api_req_server["population"] + # Resolve server name and population (WR team IDs vs legacy worlds) + if is_wr_team_id(server_id): + server_name = get_team_name(server_id) or f"Team {server_id}" + population = "N/A" + else: + api_req_server = await gw2_api.call_api(f"worlds/{server_id}", api_key) + server_name = api_req_server["name"] + population = api_req_server["population"] # Create base embed color = ctx.bot.settings["gw2"]["EmbedColor"] @@ -131,6 +136,12 @@ async def account(ctx): embed.add_field(name="Commander Tag", value=chat_formatting.inline(is_commander)) embed.add_field(name="Server", value=chat_formatting.inline(f"{server_name} ({population})")) + # Add WvW Team field if available + wvw_team_id = api_req_acc.get("wvw", {}).get("team_id") + if wvw_team_id: + team_name = get_team_name(wvw_team_id) or f"Team {wvw_team_id}" + embed.add_field(name="WvW Team", value=chat_formatting.inline(team_name)) + # Prepare optional API calls based on permissions optional_tasks = [] @@ -171,7 +182,7 @@ async def account(ctx): name="Achievements Points", value=chat_formatting.inline(str(achiev_points)), inline=False ) - wvwrank = api_req_acc["wvw_rank"] + wvwrank = api_req_acc.get("wvw", {}).get("rank") or api_req_acc.get("wvw_rank", 0) wvw_title = gw2_utils.get_wvw_rank_title(int(wvwrank)) embed.add_field( name="WvW Rank", value=chat_formatting.inline(f"{wvw_title} ({wvwrank})"), inline=False diff --git a/src/gw2/cogs/characters.py b/src/gw2/cogs/characters.py index 9f102405..61bd8dcb 100644 --- a/src/gw2/cogs/characters.py +++ b/src/gw2/cogs/characters.py @@ -9,7 +9,7 @@ class GW2Characters(GuildWars2): - """(Commands related to users characters)""" + """Guild Wars 2 commands for character information.""" def __init__(self, bot): super().__init__(bot) @@ -18,8 +18,11 @@ def __init__(self, bot): @GW2Characters.gw2.command() @commands.cooldown(1, GW2CoolDowns.Characters.seconds, commands.BucketType.user) async def characters(ctx): - """(General information about your GW2 characters) - Required API permissions: account + """Display information about your Guild Wars 2 characters. + + Required API permissions: account, characters + + Usage: gw2 characters """ @@ -28,8 +31,8 @@ async def characters(ctx): rs = await gw2_key_dal.get_api_key_by_user(ctx.message.author.id) if not rs: msg = gw2_messages.NO_API_KEY - msg += gw2_messages.KEY_ADD_INFO_HELP.format(ctx.prefix) - msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) + msg += gw2_messages.key_add_info_help(ctx.prefix) + msg += gw2_messages.key_more_info_help(ctx.prefix) return await bot_utils.send_error_msg(ctx, msg) api_key = str(rs[0]["key"]) @@ -37,9 +40,9 @@ async def characters(ctx): is_valid_key = await gw2_api.check_api_key(api_key) if not isinstance(is_valid_key, dict): msg = f"{is_valid_key.args[1]}\n" - msg += gw2_messages.INVALID_API_KEY_HELP_MESSAGE.format(ctx.prefix) - msg += gw2_messages.KEY_ADD_INFO_HELP.format(ctx.prefix) - msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) + msg += gw2_messages.INVALID_API_KEY_HELP_MESSAGE + msg += gw2_messages.key_add_info_help(ctx.prefix) + msg += gw2_messages.key_more_info_help(ctx.prefix) return await bot_utils.send_error_msg(ctx, msg) permissions = str(rs[0]["permissions"]) diff --git a/src/gw2/cogs/config.py b/src/gw2/cogs/config.py index 51107aa9..68f22b4f 100644 --- a/src/gw2/cogs/config.py +++ b/src/gw2/cogs/config.py @@ -9,7 +9,7 @@ class GW2Config(GuildWars2): - """(Guild Wars 2 Configuration Commands - Admin)""" + """Guild Wars 2 configuration commands for server settings management.""" def __init__(self, bot): super().__init__(bot) @@ -18,9 +18,11 @@ def __init__(self, bot): @GuildWars2.gw2.group() @Checks.check_is_admin() async def config(ctx): - """(Guild Wars 2 Configuration Commands - Admin) - gw2 config list - gw2 config session [on | off] + """Guild Wars 2 server configuration commands. + + Available subcommands: + gw2 config list - List all GW2 configurations + gw2 config session [on | off] - Toggle session recording """ await bot_utils.invoke_subcommand(ctx, "gw2 config") @@ -29,8 +31,10 @@ async def config(ctx): @config.command(name="list") @commands.cooldown(1, GW2CoolDowns.Config.seconds, commands.BucketType.user) async def config_list(ctx): - """(List all Guild Wars 2 Current Server Configurations) - gw2 config list + """List all Guild Wars 2 configurations for the current server. + + Usage: + gw2 config list """ color = ctx.bot.settings["gw2"]["EmbedColor"] @@ -41,7 +45,7 @@ async def config_list(ctx): name=f"{gw2_messages.CONFIG_TITLE} {ctx.guild.name}", icon_url=guild_icon_url, ) - embed.set_footer(text=gw2_messages.CONFIG_MORE_INFO.format(ctx.prefix)) + embed.set_footer(text=gw2_messages.config_more_info(ctx.prefix)) gw2_configs = Gw2ConfigsDal(ctx.bot.db_session, ctx.bot.log) rs = await gw2_configs.get_gw2_server_configs(ctx.guild.id) @@ -73,9 +77,11 @@ async def config_list(ctx): @config.command(name="session") @commands.cooldown(1, GW2CoolDowns.Config.seconds, commands.BucketType.user) async def config_session(ctx, subcommand_passed: str): - """(Configure Guild Wars 2 Sessions) - gw2 config session on - gw2 config session off + """Toggle Guild Wars 2 session recording. + + Usage: + gw2 config session on + gw2 config session off """ match subcommand_passed: @@ -127,9 +133,9 @@ async def _handle_update( # Set updating state and disable all buttons self._updating = True for item in self.children: - if hasattr(item, 'disabled'): + if hasattr(item, "disabled"): item.disabled = True - if hasattr(item, 'style'): + if hasattr(item, "style"): item.style = discord.ButtonStyle.gray # Defer the response to allow editing the original message @@ -190,7 +196,7 @@ async def _handle_update( async def _restore_buttons(self): """Restore button states and colors.""" for item in self.children: - if hasattr(item, 'disabled'): + if hasattr(item, "disabled"): item.disabled = False # Restore original button colors @@ -208,7 +214,7 @@ async def _create_updated_embed(self): name=f"{gw2_messages.CONFIG_TITLE} {self.ctx.guild.name}", icon_url=guild_icon_url, ) - embed.set_footer(text=gw2_messages.CONFIG_MORE_INFO.format(self.ctx.prefix)) + embed.set_footer(text=gw2_messages.config_more_info(self.ctx.prefix)) # Format status indicators on = chat_formatting.green_text("ON") diff --git a/src/gw2/cogs/gw2.py b/src/gw2/cogs/gw2.py index d6f94b3f..20eae223 100644 --- a/src/gw2/cogs/gw2.py +++ b/src/gw2/cogs/gw2.py @@ -3,20 +3,27 @@ class GuildWars2(commands.Cog): + """Guild Wars 2 commands for account management, WvW, and wiki search.""" + def __init__(self, bot): self.bot = bot @commands.group(name="gw2") async def gw2(self, ctx): - """(Guild Wars 2 Commands) - gw2 config list - gw2 config session [on | off] - gw2 wvw [match | info | kdr] world_name - gw2 key [add | remove | info] api_key - gw2 account - gw2 worlds - gw2 wiki name_to_search - gw2 info info_to_search + """Guild Wars 2 commands. + + Available subcommands: + gw2 config list - List all GW2 configurations + gw2 config session [on | off] - Toggle session recording + gw2 wvw [match | info | kdr] - WvW match information + gw2 key [add | update | info] - Manage API keys + gw2 key remove - Remove your API key + gw2 account - Show account information + gw2 characters - Show character information + gw2 session - Show last game session + gw2 worlds [na | eu] - List all worlds + gw2 wiki - Search the GW2 wiki + gw2 info - Info about a name/skill/rune """ await bot_utils.invoke_subcommand(ctx, "gw2") diff --git a/src/gw2/cogs/key.py b/src/gw2/cogs/key.py index e43c63f8..7f6139c5 100644 --- a/src/gw2/cogs/key.py +++ b/src/gw2/cogs/key.py @@ -8,218 +8,308 @@ from src.gw2.tools.gw2_cooldowns import GW2CoolDowns -class GW2Key(GuildWars2): - """(Commands related to GW2 API keys)""" - - def __init__(self, bot): - super().__init__(bot) - - -@GW2Key.gw2.group() -async def key(ctx): - """(Commands related to GW2 API keys) - To generate an API key, head to https://account.arena.net, and log in. - In the "Applications" tab, generate a new key with all permissions. - Required API permissions: account +def _get_user_id(ctx_or_interaction): + if isinstance(ctx_or_interaction, discord.Interaction): + return ctx_or_interaction.user.id + return ctx_or_interaction.message.author.id - Note: Only one API key per user is supported. - - gw2 key add (Adds your first GW2 API key) - gw2 key update (Updates/replaces your existing API key) - gw2 key remove (Removes your GW2 API key from the bot) - gw2 key info (Shows information about your GW2 API key) - """ - - await bot_utils.invoke_subcommand(ctx, "gw2 key") +async def _send_success(ctx_or_interaction, msg, color): + if isinstance(ctx_or_interaction, discord.Interaction): + embed = discord.Embed(description=msg, color=color) + await ctx_or_interaction.followup.send(embed=embed, ephemeral=True) + else: + await bot_utils.send_msg(ctx_or_interaction, msg, True, color) -@key.command(name="add") -@commands.cooldown(1, GW2CoolDowns.ApiKeys.seconds, commands.BucketType.user) -async def add(ctx, api_key: str): - """(Adds your first GW2 API key) - This command only works if you don't have an existing key. - Required API permissions: account - gw2 key add - """ +async def _send_error(ctx_or_interaction, msg): + msg = str(msg) + if isinstance(ctx_or_interaction, discord.Interaction): + embed = discord.Embed( + description=chat_formatting.error(msg), + color=discord.Color.red(), + ) + await ctx_or_interaction.followup.send(embed=embed, ephemeral=True) + else: + await bot_utils.send_error_msg(ctx_or_interaction, msg, True) - await bot_utils.delete_message(ctx, warning=True) - user_id = ctx.message.author.id - embed_color = ctx.bot.settings["gw2"]["EmbedColor"] - # checking API Key with gw2 servers - gw2_api = Gw2Client(ctx.bot) +async def _validate_api_key(bot, api_key): + """Validate API key with GW2 servers and return account info dict.""" + gw2_api = Gw2Client(bot) is_valid_key = await gw2_api.check_api_key(api_key) if not isinstance(is_valid_key, dict): - return await bot_utils.send_error_msg(ctx, f"{is_valid_key.args[1]}\n`{api_key}`", True) + raise ValueError(f"{is_valid_key.args[1]}\n`{api_key}`") key_name = is_valid_key["name"] permissions = ",".join(is_valid_key["permissions"]) - try: - # getting gw2 acc name - api_req_acc_info = await gw2_api.call_api("account", api_key) - gw2_acc_name = api_req_acc_info["name"] - member_server_id = api_req_acc_info["world"] - except Exception as e: - await bot_utils.send_error_msg(ctx, e, True) - return ctx.bot.log.error(ctx, e) + api_req_acc_info = await gw2_api.call_api("account", api_key) + gw2_acc_name = api_req_acc_info["name"] + member_server_id = api_req_acc_info["world"] - try: - # getting gw2 server name - uri = f"worlds/{member_server_id}" - api_req_server = await gw2_api.call_api(uri, api_key) - gw2_server_name = api_req_server["name"] - except Exception as e: - await bot_utils.send_error_msg(ctx, e, True) - ctx.bot.log.error(ctx, e) - return None + uri = f"worlds/{member_server_id}" + api_req_server = await gw2_api.call_api(uri, api_key) + gw2_server_name = api_req_server["name"] - api_key_args = { - "user_id": user_id, + return { "key_name": key_name, - "gw2_acc_name": gw2_acc_name, - "server_name": gw2_server_name, "permissions": permissions, - "api_key": api_key, + "gw2_acc_name": gw2_acc_name, + "gw2_server_name": gw2_server_name, } - # searching if API key in local database - gw2_key_dal = Gw2KeyDal(ctx.bot.db_session, ctx.bot.log) - # Check if user already has any API key - ADD command should only work for first-time users +async def _process_add_key(ctx_or_interaction, api_key, bot, prefix): + """Validate and add a new GW2 API key for the user.""" + user_id = _get_user_id(ctx_or_interaction) + embed_color = bot.settings["gw2"]["EmbedColor"] + + try: + key_info = await _validate_api_key(bot, api_key) + except Exception as e: + bot.log.error(f"API key validation failed for user {user_id}: {e}") + return await _send_error(ctx_or_interaction, e) + + gw2_key_dal = Gw2KeyDal(bot.db_session, bot.log) + existing_user_key = await gw2_key_dal.get_api_key_by_user(user_id) if existing_user_key: error_msg = ( - "❌ **You already have an API key registered.**\n\n" + f"You already have an API key registered.\n" f"Current key: `{existing_user_key[0]['name']}` for account `{existing_user_key[0]['gw2_acc_name']}`\n\n" - "**Options:**\n" - f"• To update your key: `{ctx.prefix}gw2 key update `\n" - f"• To view your current key: `{ctx.prefix}gw2 key info`\n" - f"• To remove your key first: `{ctx.prefix}gw2 key remove`\n\n" - "💡 **Tip:** Use `update` command to replace your existing key." + f"To update your key: `{prefix}gw2 key update `\n" + f"To view your current key: `{prefix}gw2 key info`\n" + f"To remove your key first: `{prefix}gw2 key remove`" ) - await bot_utils.send_error_msg(ctx, error_msg, True) - return None + return await _send_error(ctx_or_interaction, error_msg) - # Check if this exact API key is already used by someone else rs = await gw2_key_dal.get_api_key(api_key) if rs: - await bot_utils.send_error_msg(ctx, gw2_messages.KEY_ALREADY_IN_USE, True) - return None + return await _send_error(ctx_or_interaction, gw2_messages.KEY_ALREADY_IN_USE) - # If we get here, user has no existing key and the API key is not in use try: - await gw2_key_dal.insert_api_key(api_key_args) - msg = gw2_messages.KEY_ADDED_SUCCESSFULLY.format(key_name, gw2_server_name) - msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) - await bot_utils.send_msg(ctx, msg, True, embed_color) - return None - except Exception as e: - ctx.bot.log.error(f"Error inserting API key for user {user_id}: {e}") - error_msg = ( - "❌ **Failed to add API key.**\n\n" - "This could be due to a database constraint or connection issue. " - "Please try again later or contact an administrator if the problem persists." + await gw2_key_dal.insert_api_key( + { + "user_id": user_id, + "key_name": key_info["key_name"], + "gw2_acc_name": key_info["gw2_acc_name"], + "server_name": key_info["gw2_server_name"], + "permissions": key_info["permissions"], + "api_key": api_key, + } ) - await bot_utils.send_error_msg(ctx, error_msg, True) - return None - + msg = gw2_messages.key_added_successfully(key_info["key_name"], key_info["gw2_server_name"]) + msg += gw2_messages.key_more_info_help(prefix) + await _send_success(ctx_or_interaction, msg, embed_color) + except Exception as e: + bot.log.error(f"Error inserting API key for user {user_id}: {e}") + await _send_error(ctx_or_interaction, "Failed to add API key. Please try again later.") -@key.command(name="update", aliases=["replace"]) -@commands.cooldown(1, GW2CoolDowns.ApiKeys.seconds, commands.BucketType.user) -async def update(ctx, api_key: str): - """(Updates your existing GW2 API key) - This command only works if you already have a key registered. - Required API permissions: account - gw2 key update - """ - await bot_utils.delete_message(ctx, warning=True) - user_id = ctx.message.author.id - embed_color = ctx.bot.settings["gw2"]["EmbedColor"] +async def _process_update_key(ctx_or_interaction, api_key, bot, prefix): + """Validate and update an existing GW2 API key for the user.""" + user_id = _get_user_id(ctx_or_interaction) + embed_color = bot.settings["gw2"]["EmbedColor"] - # Check if user has an existing key - UPDATE command requires existing key - gw2_key_dal = Gw2KeyDal(ctx.bot.db_session, ctx.bot.log) + gw2_key_dal = Gw2KeyDal(bot.db_session, bot.log) existing_user_key = await gw2_key_dal.get_api_key_by_user(user_id) if not existing_user_key: error_msg = ( - "❌ **You don't have an API key registered yet.**\n\n" - "**Options:**\n" - f"• To add your first key: `{ctx.prefix}gw2 key add `\n" - f"• For more help: `{ctx.prefix}help gw2 key`\n\n" - "💡 **Tip:** Use `add` command for your first key." + f"You don't have an API key registered yet.\n\n" + f"To add your first key: `{prefix}gw2 key add `\n" + f"For more help: `{prefix}help gw2 key`" ) - await bot_utils.send_error_msg(ctx, error_msg, True) - return None - - # checking API Key with gw2 servers - gw2_api = Gw2Client(ctx.bot) - is_valid_key = await gw2_api.check_api_key(api_key) - if not isinstance(is_valid_key, dict): - return await bot_utils.send_error_msg(ctx, f"{is_valid_key.args[1]}\n`{api_key}`", True) - - key_name = is_valid_key["name"] - permissions = ",".join(is_valid_key["permissions"]) - - try: - # getting gw2 acc name - api_req_acc_info = await gw2_api.call_api("account", api_key) - gw2_acc_name = api_req_acc_info["name"] - member_server_id = api_req_acc_info["world"] - except Exception as e: - await bot_utils.send_error_msg(ctx, e, True) - return ctx.bot.log.error(ctx, e) + return await _send_error(ctx_or_interaction, error_msg) try: - # getting gw2 server name - uri = f"worlds/{member_server_id}" - api_req_server = await gw2_api.call_api(uri, api_key) - gw2_server_name = api_req_server["name"] + key_info = await _validate_api_key(bot, api_key) except Exception as e: - await bot_utils.send_error_msg(ctx, e, True) - ctx.bot.log.error(ctx, e) - return None + bot.log.error(f"API key validation failed for user {user_id}: {e}") + return await _send_error(ctx_or_interaction, e) - api_key_args = { - "user_id": user_id, - "key_name": key_name, - "gw2_acc_name": gw2_acc_name, - "server_name": gw2_server_name, - "permissions": permissions, - "api_key": api_key, - } - - # Check if this exact API key is already used by someone else rs = await gw2_key_dal.get_api_key(api_key) if rs and rs[0]["user_id"] != user_id: - await bot_utils.send_error_msg(ctx, gw2_messages.KEY_ALREADY_IN_USE, True) - return None + return await _send_error(ctx_or_interaction, gw2_messages.KEY_ALREADY_IN_USE) - # Update the existing key try: - await gw2_key_dal.update_api_key(api_key_args) - old_key_name = existing_user_key[0]['name'] - msg = gw2_messages.KEY_REPLACED_SUCCESSFULLY.format(old_key_name, key_name, gw2_server_name) - msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) - await bot_utils.send_msg(ctx, msg, True, embed_color) - return None + await gw2_key_dal.update_api_key( + { + "user_id": user_id, + "key_name": key_info["key_name"], + "gw2_acc_name": key_info["gw2_acc_name"], + "server_name": key_info["gw2_server_name"], + "permissions": key_info["permissions"], + "api_key": api_key, + } + ) + old_key_name = existing_user_key[0]["name"] + msg = gw2_messages.key_replaced_successfully(old_key_name, key_info["key_name"], key_info["gw2_server_name"]) + msg += gw2_messages.key_more_info_help(prefix) + await _send_success(ctx_or_interaction, msg, embed_color) except Exception as e: - ctx.bot.log.error(f"Error updating API key for user {user_id}: {e}") - error_msg = ( - "❌ **Failed to update API key.**\n\n" - "This could be due to a database constraint or connection issue. " - "Please try again later or contact an administrator if the problem persists." + bot.log.error(f"Error updating API key for user {user_id}: {e}") + await _send_error(ctx_or_interaction, "Failed to update API key. Please try again later.") + + +class ApiKeyModal(discord.ui.Modal, title="Enter GW2 API Key"): + """Modal dialog for secure API key input.""" + + api_key_input = discord.ui.TextInput( + label="API Key", + placeholder="Paste your GW2 API key here", + style=discord.TextStyle.short, + required=True, + min_length=10, + ) + + def __init__(self, bot, mode: str, prefix: str): + super().__init__() + self.bot = bot + self.mode = mode + self.prefix = prefix + + async def on_submit(self, interaction: discord.Interaction): + await interaction.response.defer(ephemeral=True) + api_key = self.api_key_input.value.strip() + if self.mode == "add": + await _process_add_key(interaction, api_key, self.bot, self.prefix) + else: + await _process_update_key(interaction, api_key, self.bot, self.prefix) + + +class ApiKeyView(discord.ui.View): + """View with a button that opens the API key modal.""" + + def __init__(self, bot, mode: str, prefix: str): + super().__init__(timeout=300) + self.bot = bot + self.mode = mode + self.prefix = prefix + self.message = None + + @discord.ui.button(label="Enter API Key", emoji="\U0001f511", style=discord.ButtonStyle.primary) + async def enter_key(self, interaction: discord.Interaction, button: discord.ui.Button): + modal = ApiKeyModal(self.bot, self.mode, self.prefix) + await interaction.response.send_modal(modal) + + async def on_timeout(self): + for item in self.children: + item.disabled = True + try: + if self.message: + await self.message.edit(view=self) + except discord.NotFound, discord.HTTPException: + pass + + +class GW2Key(GuildWars2): + """Guild Wars 2 commands for API key management.""" + + def __init__(self, bot): + super().__init__(bot) + + +@GW2Key.gw2.group() +async def key(ctx): + """Manage your Guild Wars 2 API keys. + + To generate an API key, head to https://account.arena.net and log in. + In the "Applications" tab, generate a new key with all permissions. + Only one API key per user is supported. + + Available subcommands: + gw2 key add [api_key] - Add your first GW2 API key + gw2 key update [api_key] - Update your existing API key + gw2 key remove - Remove your GW2 API key + gw2 key info - Show your API key information + """ + + await bot_utils.invoke_subcommand(ctx, "gw2 key") + + +@key.command(name="add") +@commands.cooldown(1, GW2CoolDowns.ApiKeys.seconds, commands.BucketType.user) +async def add(ctx, api_key: str = None): + """Add your first Guild Wars 2 API key. + + This command only works if you don't have an existing key. + If no key is provided, a secure input dialog will be sent to your DM. + Required API permissions: account + + Usage: + gw2 key add + gw2 key add XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + """ + + if api_key is None: + view = ApiKeyView(ctx.bot, "add", ctx.prefix) + embed = discord.Embed( + description=( + "Click the button below to securely enter your GW2 API key.\n\n" + "To generate an API key, head to https://account.arena.net\n" + "In the **Applications** tab, generate a new key with **account** permissions." + ), + color=ctx.bot.settings["gw2"]["EmbedColor"], ) - await bot_utils.send_error_msg(ctx, error_msg, True) - return None + message = await ctx.author.send(embed=embed, view=view) + view.message = message + if not bot_utils.is_private_message(ctx): + notification = discord.Embed( + description="\U0001f4ec Secure API key input sent to your DM", + color=discord.Color.green(), + ) + await ctx.send(embed=notification) + return + + await bot_utils.delete_message(ctx, warning=True) + await _process_add_key(ctx, api_key, ctx.bot, ctx.prefix) + + +@key.command(name="update", aliases=["replace"]) +@commands.cooldown(1, GW2CoolDowns.ApiKeys.seconds, commands.BucketType.user) +async def update(ctx, api_key: str = None): + """Update your existing Guild Wars 2 API key. + + This command only works if you already have a key registered. + If no key is provided, a secure input dialog will be sent to your DM. + Required API permissions: account + + Usage: + gw2 key update + gw2 key update XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + """ + + if api_key is None: + view = ApiKeyView(ctx.bot, "update", ctx.prefix) + embed = discord.Embed( + description=( + "Click the button below to securely enter your new GW2 API key.\n\n" + "This will replace your existing API key." + ), + color=ctx.bot.settings["gw2"]["EmbedColor"], + ) + message = await ctx.author.send(embed=embed, view=view) + view.message = message + if not bot_utils.is_private_message(ctx): + notification = discord.Embed( + description="\U0001f4ec Secure API key input sent to your DM", + color=discord.Color.green(), + ) + await ctx.send(embed=notification) + return + + await bot_utils.delete_message(ctx, warning=True) + await _process_update_key(ctx, api_key, ctx.bot, ctx.prefix) @key.command(name="remove") @commands.cooldown(1, GW2CoolDowns.ApiKeys.seconds, commands.BucketType.user) async def remove(ctx): - """(Removes your GW2 API key from the bot) - gw2 key remove + """Remove your Guild Wars 2 API key from the bot. + + Usage: + gw2 key remove """ user_id = ctx.message.author.id @@ -227,9 +317,9 @@ async def remove(ctx): rs = await gw2_key_dal.get_api_key_by_user(user_id) if not rs: - msg = gw2_messages.NO_API_KEY.format(ctx.prefix) - msg += gw2_messages.KEY_ADD_INFO_HELP.format(ctx.prefix) - msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) + msg = gw2_messages.NO_API_KEY + msg += gw2_messages.key_add_info_help(ctx.prefix) + msg += gw2_messages.key_more_info_help(ctx.prefix) return await bot_utils.send_error_msg(ctx, msg) else: color = ctx.bot.settings["gw2"]["EmbedColor"] @@ -241,8 +331,10 @@ async def remove(ctx): @key.command(name="info", aliases=["list"]) @commands.cooldown(1, GW2CoolDowns.ApiKeys.seconds, commands.BucketType.user) async def info(ctx): - """(Shows information about your GW2 API key) - gw2 key info + """Display information about your Guild Wars 2 API key. + + Usage: + gw2 key info """ user_id = ctx.message.author.id diff --git a/src/gw2/cogs/misc.py b/src/gw2/cogs/misc.py index c0306de9..05003503 100644 --- a/src/gw2/cogs/misc.py +++ b/src/gw2/cogs/misc.py @@ -10,7 +10,7 @@ class GW2Misc(GuildWars2): - """(Commands related to GW2)""" + """Guild Wars 2 miscellaneous commands for wiki search and item info.""" def __init__(self, bot): super().__init__(bot) @@ -19,8 +19,11 @@ def __init__(self, bot): @GW2Misc.gw2.command() @commands.cooldown(1, GW2CoolDowns.Misc.seconds, commands.BucketType.user) async def wiki(ctx, *, search): - """(Search the Guild wars 2 wiki) - gw2 wiki name_to_search + """Search the Guild Wars 2 wiki. + + Usage: + gw2 wiki elementalist + gw2 wiki ascended armor """ if len(search) > 300: @@ -35,7 +38,7 @@ async def wiki(ctx, *, search): await ctx.message.channel.typing() async with ctx.bot.aiosession.get(full_wiki_url) as r: results = await r.text() - soup = BeautifulSoup(results, 'html.parser') + soup = BeautifulSoup(results, "html.parser") posts = soup.find_all("div", {"class": "mw-search-result-heading"})[:50] total_posts = len(posts) if not posts: @@ -54,7 +57,7 @@ async def wiki(ctx, *, search): while i <= times_to_run: post = posts[i] post = post.a - url = wiki_url + post['href'] + url = wiki_url + post["href"] url = url.replace(")", "\\)") keyword = search.lower().replace("+", " ") found = False @@ -75,7 +78,7 @@ async def wiki(ctx, *, search): except IndexError: pass - embed.description = gw2_messages.DISPLAYIN_WIKI_SEARCH_TITLE.format(len(embed.fields), keyword.title()) + embed.description = gw2_messages.displaying_wiki_search_title(len(embed.fields), keyword.title()) else: embed.add_field(name=gw2_messages.NO_RESULTS, value=f"[{gw2_messages.CLICK_HERE}]({full_wiki_url})") @@ -86,8 +89,13 @@ async def wiki(ctx, *, search): @GW2Misc.gw2.command() @commands.cooldown(1, GW2CoolDowns.Misc.seconds, commands.BucketType.user) async def info(ctx, *, skill): - """(Information about a given name/skill/rune) - gw2 info info_to_search + """Display information about a given name, skill, or rune. + + Shows wiki description and Trading Post prices when available. + + Usage: + gw2 info Eternity + gw2 info Superior Rune of the Scholar """ await ctx.message.channel.typing() @@ -108,7 +116,7 @@ async def info(ctx, *, skill): skill_icon_url = "" results = await r.text() - soup = BeautifulSoup(results, 'html.parser') + soup = BeautifulSoup(results, "html.parser") for br in soup.find_all("br"): br.replace_with("\n") @@ -141,8 +149,8 @@ async def info(ctx, *, skill): f"https://www.gw2bltc.com/en/item/{item_id}-{skill_sanitized.replace('_', '-').lower()}" ) tp_results = await tp_r.text() - sell_td = BeautifulSoup(tp_results, 'html.parser').find_all("td", {"id": "sell-price"}) - buy_td = BeautifulSoup(tp_results, 'html.parser').find_all("td", {"id": "buy-price"}) + sell_td = BeautifulSoup(tp_results, "html.parser").find_all("td", {"id": "sell-price"}) + buy_td = BeautifulSoup(tp_results, "html.parser").find_all("td", {"id": "buy-price"}) tp_sell_price = gw2_utils.format_gold(sell_td[0]["data-price"]) tp_buy_price = gw2_utils.format_gold(buy_td[0]["data-price"]) skill_description = ( diff --git a/src/gw2/cogs/sessions.py b/src/gw2/cogs/sessions.py index 110bb476..41a6d8d1 100644 --- a/src/gw2/cogs/sessions.py +++ b/src/gw2/cogs/sessions.py @@ -12,7 +12,7 @@ class GW2Session(GuildWars2): - """(Commands related to GW2 player last game session)""" + """Guild Wars 2 commands for player session tracking.""" def __init__(self, bot): super().__init__(bot) @@ -21,19 +21,18 @@ def __init__(self, bot): @GW2Session.gw2.command() @commands.cooldown(1, GW2CoolDowns.Session.seconds, commands.BucketType.user) async def session(ctx): - """(Info about the gw2 player last game session) + """Display information about your last Guild Wars 2 game session. - Your API Key needs to have the following permissions: - Account, Characters, Progression, Wallet - 60 secs default cooldown + Required API permissions: Account, Characters, Progression, Wallet Requirements: - 1) Start discord, make sure you are not set to invisible - 1) Add GW2 API Key (gw2 key add api_key) - 2) Need to show, on discord, that you are playing Guild Wars 2, change this on options - 3) Start gw2 + 1) Start Discord, make sure you are not set to invisible + 2) Add GW2 API Key (gw2 key add api_key) + 3) Show on Discord that you are playing Guild Wars 2 + 4) Start GW2 - gw2 session + Usage: + gw2 session """ user_id = ctx.message.author.id @@ -41,14 +40,14 @@ async def session(ctx): rs_api_key = await gw2_key_dal.get_api_key_by_user(user_id) if not rs_api_key: msg = gw2_messages.NO_API_KEY - msg += gw2_messages.KEY_ADD_INFO_HELP.format(ctx.prefix) - msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) + msg += gw2_messages.key_add_info_help(ctx.prefix) + msg += gw2_messages.key_more_info_help(ctx.prefix) return await bot_utils.send_error_msg(ctx, msg) gw2_configs = Gw2ConfigsDal(ctx.bot.db_session, ctx.bot.log) rs_gw2_sc = await gw2_configs.get_gw2_server_configs(ctx.guild.id) if len(rs_gw2_sc) == 0 or (len(rs_gw2_sc) > 0 and not rs_gw2_sc[0]["session"]): - return await bot_utils.send_warning_msg(ctx, gw2_messages.SESSION_NOT_ACTIVE.format(ctx.prefix)) + return await bot_utils.send_warning_msg(ctx, gw2_messages.session_not_active(ctx.prefix)) api_key = rs_api_key[0]["key"] gw2_server = rs_api_key[0]["server"] @@ -75,8 +74,8 @@ async def session(ctx): error_msg += "- wallet is OK\n" if wallet is True else "- wallet is MISSING\n" error_msg += ( f"{gw2_messages.ADD_RIGHT_API_KEY_PERMISSIONS}\n" - f"{gw2_messages.KEY_ADD_INFO_HELP}" - f"{gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix)}" + f"{gw2_messages.key_add_info_help(ctx.prefix)}" + f"{gw2_messages.key_more_info_help(ctx.prefix)}" ) return await bot_utils.send_error_msg(ctx, error_msg) diff --git a/src/gw2/cogs/worlds.py b/src/gw2/cogs/worlds.py index 6f65bbef..ddd80302 100644 --- a/src/gw2/cogs/worlds.py +++ b/src/gw2/cogs/worlds.py @@ -1,4 +1,3 @@ -import asyncio import discord from discord.ext import commands from src.bot.tools import bot_utils, chat_formatting @@ -9,8 +8,58 @@ from src.gw2.tools.gw2_cooldowns import GW2CoolDowns +class EmbedPaginatorView(discord.ui.View): + """Interactive pagination view for embed pages with Previous/Next buttons.""" + + def __init__(self, pages: list[discord.Embed], author_id: int): + super().__init__(timeout=300) + self.pages = pages + self.current_page = 0 + self.author_id = author_id + self.message: discord.Message | None = None + self._update_buttons() + + def _update_buttons(self): + self.previous_button.disabled = self.current_page == 0 + self.page_indicator.label = f"{self.current_page + 1}/{len(self.pages)}" + self.next_button.disabled = self.current_page == len(self.pages) - 1 + + @discord.ui.button(label="\u25c0", style=discord.ButtonStyle.secondary) + async def previous_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id != self.author_id: + return await interaction.response.send_message( + "Only the command invoker can use these buttons.", ephemeral=True + ) + self.current_page -= 1 + self._update_buttons() + await interaction.response.edit_message(embed=self.pages[self.current_page], view=self) + + @discord.ui.button(label="1/1", style=discord.ButtonStyle.secondary, disabled=True) + async def page_indicator(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer() + + @discord.ui.button(label="\u25b6", style=discord.ButtonStyle.secondary) + async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button): + if interaction.user.id != self.author_id: + return await interaction.response.send_message( + "Only the command invoker can use these buttons.", ephemeral=True + ) + self.current_page += 1 + self._update_buttons() + await interaction.response.edit_message(embed=self.pages[self.current_page], view=self) + + async def on_timeout(self): + for item in self.children: + item.disabled = True + try: + if self.message: + await self.message.edit(view=self) + except discord.NotFound, discord.HTTPException: + pass + + class GW2Worlds(GuildWars2): - """(Guild Wars 2 List of Worlds Commands)""" + """Guild Wars 2 commands for listing worlds and WvW tiers.""" def __init__(self, bot): super().__init__(bot) @@ -18,9 +67,11 @@ def __init__(self, bot): @GW2Worlds.gw2.group() async def worlds(ctx): - """(List all worlds) - gw2 worlds na - gw2 worlds eu + """List all Guild Wars 2 worlds by region. + + Available subcommands: + gw2 worlds na - List all NA worlds with WvW tier + gw2 worlds eu - List all EU worlds with WvW tier """ await bot_utils.invoke_subcommand(ctx, "gw2 worlds") @@ -29,8 +80,10 @@ async def worlds(ctx): @worlds.command(name="na") @commands.cooldown(1, GW2CoolDowns.Worlds.seconds, commands.BucketType.user) async def worlds_na(ctx): - """(List all NA worlds and wvw tier) - gw2 worlds na + """List all North American worlds with WvW tier and population. + + Usage: + gw2 worlds na """ result, worlds_ids = await gw2_utils.get_worlds_ids(ctx) @@ -72,8 +125,10 @@ async def worlds_na(ctx): @worlds.command(name="eu") @commands.cooldown(1, GW2CoolDowns.Worlds.seconds, commands.BucketType.user) async def worlds_eu(ctx): - """(List all EU worlds and wvw tier) - gw2 worlds eu + """List all European worlds with WvW tier and population. + + Usage: + gw2 worlds eu """ result, worlds_ids = await gw2_utils.get_worlds_ids(ctx) @@ -114,7 +169,7 @@ async def worlds_eu(ctx): async def _send_paginated_worlds_embed(ctx, embed): """ - Send worlds with pagination using reactions for navigation + Send worlds with pagination using buttons for navigation """ max_fields = 25 color = ctx.bot.settings["gw2"]["EmbedColor"] @@ -138,80 +193,16 @@ async def _send_paginated_worlds_embed(ctx, embed): page_number = (i // max_fields) + 1 total_pages = (total_fields + max_fields - 1) // max_fields - - # Check if we're in a DM for the footer message - if isinstance(ctx.channel, discord.DMChannel): - footer_text = ( - f"Page {page_number}/{total_pages} • Use reactions to navigate (reactions won't disappear in DMs)" - ) - else: - footer_text = f"Page {page_number}/{total_pages}" - - page_embed.set_footer(text=footer_text) + page_embed.set_footer(text=f"Page {page_number}/{total_pages}") pages.append(page_embed) if len(pages) == 1: await ctx.send(embed=pages[0]) return - # Check if we're in a DM channel - is_dm = isinstance(ctx.channel, discord.DMChannel) - - # Send first page with reactions - current_page = 0 - message = await ctx.send(embed=pages[current_page]) - - # Add reaction controls with small delays to ensure they're all added - try: - await message.add_reaction("⬅️") - await asyncio.sleep(0.2) - await message.add_reaction("➡️") - await asyncio.sleep(0.2) - except discord.HTTPException as e: - ctx.bot.log.error(f"Failed to add pagination reactions: {e}") - # Send without pagination if reactions fail - await ctx.send(embed=pages[0]) - return - - def check(react, react_user): - return ( - react_user == ctx.author - and react.message.id == message.id - and str(react.emoji) in ["⬅️", "➡️"] - and not react_user.bot # Ensure it's not a bot reaction - ) - - timeout = 60 - - try: - while True: - reaction, user = await ctx.bot.wait_for("reaction_add", timeout=timeout, check=check) - - emoji_str = str(reaction.emoji) - - if emoji_str == "➡️" and current_page < len(pages) - 1: - current_page += 1 - await message.edit(embed=pages[current_page]) - elif emoji_str == "⬅️" and current_page > 0: - current_page -= 1 - await message.edit(embed=pages[current_page]) - - # Remove user's reaction to keep the interface clean (skip in DM channels) - if not is_dm: - try: - await message.remove_reaction(reaction.emoji, user) - except discord.Forbidden, discord.NotFound, discord.HTTPException: - pass # Silently handle permission issues - - except TimeoutError: - pass # Silently handle timeout - - # Clean up by removing all reactions (skip in DM channels) - if not is_dm: - try: - await message.clear_reactions() - except discord.Forbidden: - pass # Silently handle permission issues + view = EmbedPaginatorView(pages, ctx.author.id) + msg = await ctx.send(embed=pages[0], view=view) + view.message = msg async def setup(bot): diff --git a/src/gw2/cogs/wvw.py b/src/gw2/cogs/wvw.py index 0eb0d484..bfc5a22b 100644 --- a/src/gw2/cogs/wvw.py +++ b/src/gw2/cogs/wvw.py @@ -4,6 +4,7 @@ from src.database.dal.gw2.gw2_key_dal import Gw2KeyDal from src.gw2.cogs.gw2 import GuildWars2 from src.gw2.constants import gw2_messages +from src.gw2.constants.gw2_teams import get_team_name, is_wr_team_id from src.gw2.tools import gw2_utils from src.gw2.tools.gw2_client import Gw2Client from src.gw2.tools.gw2_cooldowns import GW2CoolDowns @@ -11,66 +12,89 @@ class GW2WvW(GuildWars2): - """(Commands related to GW2 World versus World)""" + """Guild Wars 2 World vs World commands.""" def __init__(self, bot): super().__init__(bot) @GuildWars2.gw2.group() async def wvw(self, ctx): - """(Guild Wars 2 Configuration Commands - Admin) - gw2 wvw info world_name - gw2 wvw match world_name - gw2 wvw kdr world_name + """Guild Wars 2 World vs World commands. + + Available subcommands: + gw2 wvw info [world] - Info about a WvW world + gw2 wvw match [world] - WvW match scores + gw2 wvw kdr [world] - WvW kill/death ratios """ await bot_utils.invoke_subcommand(ctx, "gw2 wvw") + async def _resolve_wvw_world_id(self, ctx, gw2_api, world, error_msg): + """Resolve a WvW world/team ID from world name or account data. + + Returns the world/team ID, or None if resolution failed (error already sent). + """ + if world: + return await gw2_utils.get_world_id(self.bot, world) + + try: + gw2_key_dal = Gw2KeyDal(self.bot.db_session, self.bot.log) + rs = await gw2_key_dal.get_api_key_by_user(ctx.message.author.id) + if not rs: + await bot_utils.send_error_msg(ctx, error_msg) + return None + + api_key = rs[0]["key"] + results = await gw2_api.call_api("account", api_key) + # Prefer WR team_id over legacy world + return results.get("wvw", {}).get("team_id") or results["world"] + except APIKeyError: + await bot_utils.send_error_msg(ctx, error_msg) + return None + except Exception as e: + await bot_utils.send_error_msg(ctx, e) + self.bot.log.error(ctx, e) + return None + @wvw.command(name="info") @commands.cooldown(1, GW2CoolDowns.Wvw.seconds, commands.BucketType.user) async def info(self, ctx, *, world: str = None): + """Display WvW information for a world. Defaults to your account's world. + + Usage: + gw2 wvw info + gw2 wvw info Blackgate + """ await ctx.message.channel.typing() gw2_api = Gw2Client(self.bot) no_api_key_msg = gw2_messages.NO_API_KEY - no_api_key_msg += gw2_messages.KEY_ADD_INFO_HELP.format(ctx.prefix) - no_api_key_msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) - - if not world: - try: - gw2_key_dal = Gw2KeyDal(self.bot.db_session, self.bot.log) - rs = await gw2_key_dal.get_api_key_by_user(ctx.message.author.id) - if not rs: - return await bot_utils.send_error_msg(ctx, no_api_key_msg) - - api_key = rs[0]["key"] - results = await gw2_api.call_api("account", api_key) - wid = results["world"] - except APIKeyError: - return await bot_utils.send_error_msg(ctx, no_api_key_msg) - except Exception as e: - await bot_utils.send_error_msg(ctx, e) - return self.bot.log.error(ctx, e) - else: - wid = await gw2_utils.get_world_id(self.bot, world) + no_api_key_msg += gw2_messages.key_add_info_help(ctx.prefix) + no_api_key_msg += gw2_messages.key_more_info_help(ctx.prefix) + wid = await self._resolve_wvw_world_id(ctx, gw2_api, world, no_api_key_msg) if not wid: - return await bot_utils.send_error_msg(ctx, f"{gw2_messages.INVALID_WORLD_NAME}\n{world}") + if world: + return await bot_utils.send_error_msg(ctx, f"{gw2_messages.INVALID_WORLD_NAME}\n{world}") + return None try: await ctx.message.channel.typing() matches = await gw2_api.call_api(f"wvw/matches?world={wid}") - worldinfo = await gw2_api.call_api(f"worlds?id={wid}") + + # Resolve world info: WR team IDs vs legacy worlds + if is_wr_team_id(wid): + world_name = get_team_name(wid) or f"Team {wid}" + population = "N/A" + else: + worldinfo = await gw2_api.call_api(f"worlds?id={wid}") + world_name = worldinfo["name"] + population = worldinfo["population"] except Exception as e: await bot_utils.send_error_msg(ctx, e) return ctx.bot.log.error(ctx, e) - if wid < 2001: - tier_number = matches["id"].replace("1-", "") - tier = f"North America Tier {tier_number}" - else: - tier_number = matches["id"].replace("2-", "") - tier = f"Europe Tier {tier_number}" + tier = _resolve_tier(matches) worldcolor = None for key, value in matches["all_worlds"].items(): @@ -90,7 +114,7 @@ async def info(self, ctx, *, world: str = None): color = discord.Color.default() ppt = 0 - score = format(matches["scores"][worldcolor], ',d') + score = format(matches["scores"][worldcolor], ",d") victoryp = matches["victory_points"][worldcolor] await ctx.message.channel.typing() @@ -99,8 +123,6 @@ async def info(self, ctx, *, world: str = None): if objective["owner"].lower() == worldcolor: ppt += objective["points_tick"] - population = worldinfo["population"] - if population == "VeryHigh": population = "Very high" @@ -113,13 +135,12 @@ async def info(self, ctx, *, world: str = None): kd = round((kills / deaths), 3) skirmish_now = len(matches["skirmishes"]) - 1 - skirmish = format(matches["skirmishes"][skirmish_now]["scores"][worldcolor], ',d') + skirmish = format(matches["skirmishes"][skirmish_now]["scores"][worldcolor], ",d") - kills = format(matches["kills"][worldcolor], ',d') - deaths = format(matches["deaths"][worldcolor], ',d') - title = f"{worldinfo['name']}" + kills = format(matches["kills"][worldcolor], ",d") + deaths = format(matches["deaths"][worldcolor], ",d") - embed = discord.Embed(title=title, description=tier, color=color) + embed = discord.Embed(title=world_name, description=tier, color=color) embed.add_field(name="Score", value=chat_formatting.inline(score)) embed.add_field(name="Points per tick", value=chat_formatting.inline(ppt)) embed.add_field(name="Victory Points", value=chat_formatting.inline(victoryp)) @@ -134,50 +155,32 @@ async def info(self, ctx, *, world: str = None): @wvw.command(name="match") @commands.cooldown(1, GW2CoolDowns.Wvw.seconds, commands.BucketType.user) async def match(self, ctx, *, world: str = None): - """(Info about a wvw match. Defaults to account's world) + """Display WvW match scores. Defaults to your account's world. - gw2 match - gw2 match world_name + Usage: + gw2 wvw match + gw2 wvw match Blackgate """ await ctx.message.channel.typing() gw2_api = Gw2Client(self.bot) - if not world: - try: - gw2_key_dal = Gw2KeyDal(self.bot.db_session, self.bot.log) - rs = await gw2_key_dal.get_api_key_by_user(ctx.message.author.id) - if not rs: - msg = gw2_messages.MISSING_WORLD_NAME - msg += gw2_messages.MATCH_WORLD_NAME_HELP.format(ctx.prefix) - msg += gw2_messages.KEY_ADD_INFO_HELP.format(ctx.prefix) - msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) - return await bot_utils.send_error_msg(ctx, msg) - - api_key = rs[0]["key"] - results = await gw2_api.call_api("account", api_key) - wid = results["world"] - except APIKeyError: - return await bot_utils.send_error_msg(ctx, gw2_messages.NO_API_KEY) - except Exception as e: - await bot_utils.send_error_msg(ctx, e) - return self.bot.log.error(ctx, e) - else: - wid = await gw2_utils.get_world_id(self.bot, world) + no_key_msg = gw2_messages.MISSING_WORLD_NAME + no_key_msg += gw2_messages.match_world_name_help(ctx.prefix) + no_key_msg += gw2_messages.key_add_info_help(ctx.prefix) + no_key_msg += gw2_messages.key_more_info_help(ctx.prefix) + wid = await self._resolve_wvw_world_id(ctx, gw2_api, world, no_key_msg) if not wid: - return await bot_utils.send_error_msg(ctx, f"{gw2_messages.INVALID_WORLD_NAME}: {world}") + if world: + return await bot_utils.send_error_msg(ctx, f"{gw2_messages.INVALID_WORLD_NAME}: {world}") + return None try: await ctx.message.channel.typing() matches = await gw2_api.call_api(f"wvw/matches?world={wid}") - if wid < 2001: - tier_number = matches["id"].replace("1-", "") - tier = f"North America Tier {tier_number}" - else: - tier_number = matches["id"].replace("2-", "") - tier = f"Europe Tier {tier_number}" + tier = _resolve_tier(matches) green_worlds_names = await _get_map_names_embed_values(ctx, "green", matches) blue_worlds_names = await _get_map_names_embed_values(ctx, "blue", matches) @@ -204,48 +207,32 @@ async def match(self, ctx, *, world: str = None): @wvw.command(name="kdr") @commands.cooldown(1, GW2CoolDowns.Wvw.seconds, commands.BucketType.user) async def kdr(self, ctx, *, world: str = None): - """(Info about a wvw kdr match. Defaults to account's world) - gw2 kdr - gw2 kdr world_name + """Display WvW kill/death ratios. Defaults to your account's world. + + Usage: + gw2 wvw kdr + gw2 wvw kdr Blackgate """ await ctx.message.channel.typing() gw2_api = Gw2Client(self.bot) - if not world: - try: - gw2_key_dal = Gw2KeyDal(self.bot.db_session, self.bot.log) - rs = await gw2_key_dal.get_api_key_by_user(ctx.message.author.id) - if not rs: - msg = gw2_messages.INVALID_WORLD_NAME - msg += gw2_messages.MATCH_WORLD_NAME_HELP.format(ctx.prefix) - msg += gw2_messages.KEY_ADD_INFO_HELP.format(ctx.prefix) - msg += gw2_messages.KEY_MORE_INFO_HELP.format(ctx.prefix) - return await bot_utils.send_error_msg(ctx, msg) - api_key = rs[0]["key"] - results = await gw2_api.call_api("account", api_key) - wid = results["world"] - except APIKeyError: - return await bot_utils.send_error_msg(ctx, gw2_messages.NO_API_KEY) - except Exception as e: - await bot_utils.send_error_msg(ctx, e) - return self.bot.log.error(ctx, e) - else: - wid = await gw2_utils.get_world_id(self.bot, world) + no_key_msg = gw2_messages.INVALID_WORLD_NAME + no_key_msg += gw2_messages.match_world_name_help(ctx.prefix) + no_key_msg += gw2_messages.key_add_info_help(ctx.prefix) + no_key_msg += gw2_messages.key_more_info_help(ctx.prefix) + wid = await self._resolve_wvw_world_id(ctx, gw2_api, world, no_key_msg) if not wid: - return await bot_utils.send_error_msg(ctx, f"{gw2_messages.INVALID_WORLD_NAME}: {world}") + if world: + return await bot_utils.send_error_msg(ctx, f"{gw2_messages.INVALID_WORLD_NAME}: {world}") + return None try: await ctx.message.channel.typing() matches = await gw2_api.call_api(f"wvw/matches?world={wid}") - if wid < 2001: - tier_number = matches["id"].replace("1-", "") - tier = f"{gw2_messages.NA_TIER_TITLE} {tier_number}" - else: - tier_number = matches["id"].replace("2-", "") - tier = f"{gw2_messages.EU_TIER_TITLE}{tier_number}" + tier = _resolve_tier(matches) green_worlds_names = await _get_map_names_embed_values(ctx, "green", matches) blue_worlds_names = await _get_map_names_embed_values(ctx, "blue", matches) @@ -270,6 +257,17 @@ async def kdr(self, ctx, *, world: str = None): return None +def _resolve_tier(matches: dict) -> str: + """Resolve tier string from match ID (works for both legacy and WR matches).""" + match_id = matches["id"] + if match_id.startswith("1-"): + tier_number = match_id.replace("1-", "") + return f"{gw2_messages.NA_TIER_TITLE} {tier_number}" + else: + tier_number = match_id.replace("2-", "") + return f"{gw2_messages.EU_TIER_TITLE} {tier_number}" + + async def _get_map_names_embed_values(ctx, map_color: str, matches): primary_server_id = [] all_ids = matches["all_worlds"][map_color] diff --git a/src/gw2/constants/gw2_messages.py b/src/gw2/constants/gw2_messages.py index 5e29ee76..55e4a39b 100644 --- a/src/gw2/constants/gw2_messages.py +++ b/src/gw2/constants/gw2_messages.py @@ -17,24 +17,36 @@ ################################# API_KEY_MESSAGE_REMOVED = "Your message with your API Key was removed for privacy." API_KEY_MESSAGE_REMOVED_DENIED = ( - "Bot does not have permission to delete the message with your API key.\n" - "Missing bot permission: `Manage Messages`" + "Bot does not have permission to delete the message with your API key.\nMissing bot permission: `Manage Messages`" ) ################################# # GW2 ACCOUNT/CHARACTERS ################################# NO_API_KEY = "You dont have an API key registered.\n" -KEY_ADD_INFO_HELP = "To add or replace an API key send a DM with: `{0}gw2 key add `\n" -KEY_MORE_INFO_HELP = "To get info about your api key: `{0}gw2 key info`" INVALID_API_KEY_HELP_MESSAGE = "This API Key is INVALID or no longer exists in gw2 api database.\n" + + +def key_add_info_help(prefix: str) -> str: + return f"To add or replace an API key send a DM with: `{prefix}gw2 key add `\n" + + +def key_more_info_help(prefix: str) -> str: + return f"To get info about your api key: `{prefix}gw2 key info`" + + API_KEY_NO_PERMISSION = ( - "Your API key doesnt have permission to access your gw2 account.\n" "Please add one key with account permission." + "Your API key doesnt have permission to access your gw2 account.\nPlease add one key with account permission." ) ################################# # GW2 CONFIG ################################# CONFIG_TITLE = "Guild Wars 2 configurations for" -CONFIG_MORE_INFO = "For more info: {0}help gw2 config" + + +def config_more_info(prefix: str) -> str: + return f"For more info: {prefix}help gw2 config" + + USER_SESSION_TITLE = "GW2 Users Session" SESSION_ACTIVATED = "Last session `ACTIVATED`\nBot will now record Gw2 users last sessions." SESSION_DEACTIVATED = "Last session `DEACTIVATED`\nBot will `NOT` record Gw2 users last sessions." @@ -43,10 +55,16 @@ ################################# KEY_ALREADY_IN_USE = "That API key is already in use by someone else." KEY_REMOVED_SUCCESSFULLY = "Your GW2 API Key has been deleted successfully." -KEY_REPLACED_SUCCESSFULLY = "Your API key `{0}` was **replaced** with your new key: `{1}`\n" "Server: `{2}`\n" -KEY_ADDED_SUCCESSFULLY = ( - "Your key was verified and was **added** to your discord account.\n" "Key: `{0}`\n" "Server: `{1}`\n" -) + + +def key_replaced_successfully(old: str, new: str, server: str) -> str: + return f"Your API key `{old}` was **replaced** with your new key: `{new}`\nServer: `{server}`\n" + + +def key_added_successfully(key_name: str, server_name: str) -> str: + return f"Your key was verified and was **added** to your discord account.\nKey: `{key_name}`\nServer: `{server_name}`\n" + + ################################# # GW2 MISC ################################# @@ -54,13 +72,23 @@ WIKI_SEARCH_RESULTS = "Wiki Search Results" NO_RESULTS = "No results!" CLICK_HERE = "Click here" -DISPLAYIN_WIKI_SEARCH_TITLE = "Displaying **{0}** closest titles that matches **{1}**" + + +def displaying_wiki_search_title(count: int, keyword: str) -> str: + return f"Displaying **{count}** closest titles that matches **{keyword}**" + + CLICK_ON_LINK = "Click on link above for more info !!!" ################################# # GW2 SESSIONS ################################# SESSION_TITLE = "GW2 Last Session" -SESSION_NOT_ACTIVE = "Last session is not active on this server.\nTo activate use: `{0}gw2 config session on`" + + +def session_not_active(prefix: str) -> str: + return f"Last session is not active on this server.\nTo activate use: `{prefix}gw2 config session on`" + + SESSION_MISSING_PERMISSIONS_TITLE = "To use this command your API key needs to have the following permissions" ADD_RIGHT_API_KEY_PERMISSIONS = ( "Please add another API key with permissions that are MISSING if you want to use this command." @@ -102,7 +130,7 @@ "No records were found in your name.\n" "You are probably trying to execute this command without playing the game.\n" "Make sure your status is NOT set to invisible in discord.\n" - "Make sure \"Display current running game as a status message\" is ON.\n" + 'Make sure "Display current running game as a status message" is ON.\n' "Make sure to start discord on your Desktop FIRST before starting Guild Wars 2." ) ################################# @@ -116,7 +144,12 @@ INVALID_WORLD_NAME = "Invalid world name" MISSING_WORLD_NAME = "Missing World Name" WORLD_COLOR_ERROR = "Could not resolve world's color" -MATCH_WORLD_NAME_HELP = "Use `{0}gw2 match `\nOr register an API key on your account.\n" + + +def match_world_name_help(prefix: str) -> str: + return f"Use `{prefix}gw2 match `\nOr register an API key on your account.\n" + + WVW_KDR_TITLE = "WvW Kills/Death Ratings" NA_TIER_TITLE = "North America Tier" -EU_TIER_TITLE = "Europe America Tier" +EU_TIER_TITLE = "Europe Tier" diff --git a/src/gw2/constants/gw2_teams.py b/src/gw2/constants/gw2_teams.py new file mode 100644 index 00000000..a92fa6de --- /dev/null +++ b/src/gw2/constants/gw2_teams.py @@ -0,0 +1,49 @@ +"""World Restructuring (WR) team constants for Guild Wars 2 WvW. + +Since mid-2024, GW2 uses team-based matchmaking (World Restructuring) instead of +server-based WvW. Team IDs (11xxx for NA, 12xxx for EU) are not in the /v2/worlds +API, so names must be hardcoded. +""" + +# NA teams: 11001-11012, EU teams: 12001-12015 +WR_TEAM_NAMES: dict[int, str] = { + # North America + 11001: "Team 1 (NA)", + 11002: "Team 2 (NA)", + 11003: "Team 3 (NA)", + 11004: "Team 4 (NA)", + 11005: "Team 5 (NA)", + 11006: "Team 6 (NA)", + 11007: "Team 7 (NA)", + 11008: "Team 8 (NA)", + 11009: "Team 9 (NA)", + 11010: "Team 10 (NA)", + 11011: "Team 11 (NA)", + 11012: "Team 12 (NA)", + # Europe + 12001: "Team 1 (EU)", + 12002: "Team 2 (EU)", + 12003: "Team 3 (EU)", + 12004: "Team 4 (EU)", + 12005: "Team 5 (EU)", + 12006: "Team 6 (EU)", + 12007: "Team 7 (EU)", + 12008: "Team 8 (EU)", + 12009: "Team 9 (EU)", + 12010: "Team 10 (EU)", + 12011: "Team 11 (EU)", + 12012: "Team 12 (EU)", + 12013: "Team 13 (EU)", + 12014: "Team 14 (EU)", + 12015: "Team 15 (EU)", +} + + +def is_wr_team_id(world_id: int) -> bool: + """Check if the given ID is a World Restructuring team ID (11xxx or 12xxx).""" + return 11001 <= world_id <= 12999 + + +def get_team_name(team_id: int) -> str | None: + """Get the team name for a WR team ID, or None if not found.""" + return WR_TEAM_NAMES.get(team_id) diff --git a/src/gw2/tools/gw2_client.py b/src/gw2/tools/gw2_client.py index 1c4c0842..9a1a1232 100644 --- a/src/gw2/tools/gw2_client.py +++ b/src/gw2/tools/gw2_client.py @@ -58,20 +58,21 @@ async def _handle_api_error(self, response, endpoint): init_msg = f"{response.status})({endpoint.split('?')[0]}" - if response.status == 400: - self._handle_400_error(response.status, err_msg, init_msg) - elif response.status == 403: - self._handle_403_error(response.status, err_msg, init_msg) - elif response.status == 404: - self._handle_404_error(response.status, endpoint) - elif response.status == 429: - self._handle_429_error(init_msg) - elif response.status in (502, 504): - self._handle_502_504_error(init_msg) - elif response.status == 503: - self._handle_503_error(init_msg, err_msg) - else: - self._handle_other_error(response, init_msg, err_msg) + match response.status: + case 400: + self._handle_400_error(response.status, err_msg, init_msg) + case 403: + self._handle_403_error(response.status, err_msg, init_msg) + case 404: + self._handle_404_error(response.status, endpoint) + case 429: + self._handle_429_error(init_msg) + case 502 | 504: + self._handle_502_504_error(init_msg) + case 503: + self._handle_503_error(init_msg, err_msg) + case _: + self._handle_other_error(response, init_msg, err_msg) def _handle_400_error(self, status, err_msg, init_msg): """Handle 400 Bad Request errors.""" diff --git a/src/gw2/tools/gw2_cooldowns.py b/src/gw2/tools/gw2_cooldowns.py index a3a06e33..71f42970 100644 --- a/src/gw2/tools/gw2_cooldowns.py +++ b/src/gw2/tools/gw2_cooldowns.py @@ -16,15 +16,15 @@ class GW2CoolDowns(Enum): we use a tuple (value, unique_id) and access the actual cooldown via .value[0] """ - Account = (1 if variables.DEBUG else _gw2_settings.account_cooldown, 'account') - ApiKeys = (1 if variables.DEBUG else _gw2_settings.api_keys_cooldown, 'api_keys') - Characters = (1 if variables.DEBUG else _gw2_settings.characters_cooldown, 'characters') - Config = (1 if variables.DEBUG else _gw2_settings.config_cooldown, 'config') - Daily = (1 if variables.DEBUG else _gw2_settings.daily_cooldown, 'daily') - Misc = (1 if variables.DEBUG else _gw2_settings.misc_cooldown, 'misc') - Session = (1 if variables.DEBUG else _gw2_settings.session_cooldown, 'session') - Worlds = (1 if variables.DEBUG else _gw2_settings.worlds_cooldown, 'worlds') - Wvw = (1 if variables.DEBUG else _gw2_settings.wvw_cooldown, 'wvw') + Account = (1 if variables.DEBUG else _gw2_settings.account_cooldown, "account") + ApiKeys = (1 if variables.DEBUG else _gw2_settings.api_keys_cooldown, "api_keys") + Characters = (1 if variables.DEBUG else _gw2_settings.characters_cooldown, "characters") + Config = (1 if variables.DEBUG else _gw2_settings.config_cooldown, "config") + Daily = (1 if variables.DEBUG else _gw2_settings.daily_cooldown, "daily") + Misc = (1 if variables.DEBUG else _gw2_settings.misc_cooldown, "misc") + Session = (1 if variables.DEBUG else _gw2_settings.session_cooldown, "session") + Worlds = (1 if variables.DEBUG else _gw2_settings.worlds_cooldown, "worlds") + Wvw = (1 if variables.DEBUG else _gw2_settings.wvw_cooldown, "wvw") def __str__(self) -> str: """Return the cooldown value as a string.""" diff --git a/src/gw2/tools/gw2_exceptions.py b/src/gw2/tools/gw2_exceptions.py index 29877a9d..82191c00 100644 --- a/src/gw2/tools/gw2_exceptions.py +++ b/src/gw2/tools/gw2_exceptions.py @@ -9,7 +9,7 @@ def __init__(self, bot: Bot, msg: str): self.bot = bot self.message = msg # Log error when exception is created - if hasattr(bot, 'log') and bot.log: + if hasattr(bot, "log") and bot.log: bot.log.error(f"GW2 API Error: {msg}") diff --git a/src/gw2/tools/gw2_utils.py b/src/gw2/tools/gw2_utils.py index ea5fac4b..50315d33 100644 --- a/src/gw2/tools/gw2_utils.py +++ b/src/gw2/tools/gw2_utils.py @@ -22,6 +22,7 @@ def __init__(self): from src.database.dal.gw2.gw2_session_chars_dal import Gw2SessionCharsDal from src.database.dal.gw2.gw2_sessions_dal import Gw2SessionsDal from src.gw2.constants import gw2_messages +from src.gw2.constants.gw2_teams import get_team_name, is_wr_team_id from src.gw2.tools.gw2_client import Gw2Client @@ -200,15 +201,38 @@ async def get_world_id(bot: Bot, world: str | None) -> int | None: async def get_world_name_population(ctx: commands.Context, world_ids: str) -> list[str] | None: - """Get world names and population data.""" - try: - gw2_api = Gw2Client(ctx.bot) - results = await gw2_api.call_api(f"worlds?ids={world_ids}") + """Get world names and population data. - if not results: + Handles both legacy world IDs (1xxx/2xxx) via /v2/worlds API and + World Restructuring team IDs (11xxx/12xxx) via hardcoded lookup. + """ + try: + id_list = [int(wid.strip()) for wid in world_ids.split(",") if wid.strip()] + if not id_list: return None - return [world["name"] for world in results] + legacy_ids = [wid for wid in id_list if not is_wr_team_id(wid)] + + # Build name lookup from API for legacy IDs + legacy_names: dict[int, str] = {} + if legacy_ids: + gw2_api = Gw2Client(ctx.bot) + legacy_ids_str = ",".join(str(wid) for wid in legacy_ids) + results = await gw2_api.call_api(f"worlds?ids={legacy_ids_str}") + if results: + for world in results: + legacy_names[world["id"]] = world["name"] + + # Resolve all IDs in original order + names: list[str] = [] + for wid in id_list: + if is_wr_team_id(wid): + name = get_team_name(wid) + names.append(name if name else f"Team {wid}") + elif wid in legacy_names: + names.append(legacy_names[wid]) + + return names if names else None except Exception as e: ctx.bot.log.error(f"Error fetching world names for IDs {world_ids}: {e}") @@ -344,9 +368,10 @@ async def get_user_stats(bot: Bot, api_key: str) -> dict | None: def _create_initial_user_stats(account_data: dict) -> dict: """Create initial user stats structure.""" + wvw_rank = account_data.get("wvw", {}).get("rank") or account_data.get("wvw_rank", 0) return { "acc_name": account_data["name"], - "wvw_rank": account_data["wvw_rank"], + "wvw_rank": wvw_rank, "gold": 0, "karma": 0, "laurels": 0, @@ -438,19 +463,21 @@ def get_wvw_rank_title(rank: int) -> str: def _get_wvw_rank_prefix(rank: int) -> str: """Get WvW rank prefix (Bronze, Silver, etc.).""" - if 150 <= rank <= 619: - return "Bronze" - elif 620 <= rank <= 1394: - return "Silver" - elif 1395 <= rank <= 2544: - return "Gold" - elif 2545 <= rank <= 4094: - return "Platinum" - elif 4095 <= rank <= 6444: - return "Mithril" - elif rank >= 6445: - return "Diamond" - return "" + match rank: + case r if 150 <= r <= 619: + return "Bronze" + case r if 620 <= r <= 1394: + return "Silver" + case r if 1395 <= r <= 2544: + return "Gold" + case r if 2545 <= r <= 4094: + return "Platinum" + case r if 4095 <= r <= 6444: + return "Mithril" + case r if r >= 6445: + return "Diamond" + case _: + return "" def _get_wvw_rank_title(rank: int) -> str: @@ -490,25 +517,27 @@ def _get_wvw_rank_title(rank: int) -> str: def get_pvp_rank_title(rank: int) -> str: """Get PvP rank title based on rank number.""" - if 1 <= rank <= 9: - return "Rabbit" - elif 10 <= rank <= 19: - return "Deer" - elif 20 <= rank <= 29: - return "Dolyak" - elif 30 <= rank <= 39: - return "Wolf" - elif 40 <= rank <= 49: - return "Tiger" - elif 50 <= rank <= 59: - return "Bear" - elif 60 <= rank <= 69: - return "Shark" - elif 70 <= rank <= 79: - return "Phoenix" - elif rank >= 80: - return "Dragon" - return "" + match rank: + case r if 1 <= r <= 9: + return "Rabbit" + case r if 10 <= r <= 19: + return "Deer" + case r if 20 <= r <= 29: + return "Dolyak" + case r if 30 <= r <= 39: + return "Wolf" + case r if 40 <= r <= 49: + return "Tiger" + case r if 50 <= r <= 59: + return "Bear" + case r if 60 <= r <= 69: + return "Shark" + case r if 70 <= r <= 79: + return "Phoenix" + case r if r >= 80: + return "Dragon" + case _: + return "" def format_gold(currency: str) -> str: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index d27755e7..d2cd7657 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -32,7 +32,7 @@ def postgres_container(): from testcontainers.postgres import PostgresContainer - with PostgresContainer("postgres:17", driver=None) as pg: + with PostgresContainer("postgres:latest", driver=None) as pg: yield pg diff --git a/tests/integration/test_alembic_migrations.py b/tests/integration/test_alembic_migrations.py index d1764c4e..01eee84e 100644 --- a/tests/integration/test_alembic_migrations.py +++ b/tests/integration/test_alembic_migrations.py @@ -54,7 +54,7 @@ async def _execute(db_session, stmt): async def test_updated_at_function_exists(db_session): rows = await _fetch_rows( db_session, - text("SELECT routine_name FROM information_schema.routines " "WHERE routine_name = 'updated_at_column_func'"), + text("SELECT routine_name FROM information_schema.routines WHERE routine_name = 'updated_at_column_func'"), ) assert len(rows) == 1 assert rows[0]["routine_name"] == "updated_at_column_func" @@ -83,9 +83,7 @@ async def test_all_triggers_exist(db_session): rows = await _fetch_rows( db_session, text( - "SELECT trigger_name FROM information_schema.triggers " - "WHERE trigger_schema = 'public' " - "ORDER BY trigger_name" + "SELECT trigger_name FROM information_schema.triggers WHERE trigger_schema = 'public' ORDER BY trigger_name" ), ) trigger_names = [r["trigger_name"] for r in rows] @@ -183,7 +181,7 @@ async def test_servers_columns(db_session): async def test_servers_index_exists(db_session): rows = await _fetch_rows( db_session, - text("SELECT indexname FROM pg_indexes " "WHERE tablename = 'servers' AND indexname = 'ix_servers_id'"), + text("SELECT indexname FROM pg_indexes WHERE tablename = 'servers' AND indexname = 'ix_servers_id'"), ) assert len(rows) == 1 @@ -264,9 +262,7 @@ async def test_custom_commands_cascade_delete(db_session): await _execute(db_session, text("INSERT INTO servers (id, name) VALUES (10003, 'Cascade CC')")) await _execute( db_session, - text( - "INSERT INTO custom_commands (server_id, name, description) " "VALUES (10003, 'temp', 'will be cascaded')" - ), + text("INSERT INTO custom_commands (server_id, name, description) VALUES (10003, 'temp', 'will be cascaded')"), ) await _execute(db_session, text("DELETE FROM servers WHERE id = 10003")) rows = await _fetch_rows( @@ -318,7 +314,7 @@ async def test_profanity_filters_cascade_delete(db_session): await _execute(db_session, text("INSERT INTO servers (id, name) VALUES (10005, 'Cascade PF')")) await _execute( db_session, - text("INSERT INTO profanity_filters (server_id, channel_id, channel_name) " "VALUES (10005, 80002, 'spam')"), + text("INSERT INTO profanity_filters (server_id, channel_id, channel_name) VALUES (10005, 80002, 'spam')"), ) await _execute(db_session, text("DELETE FROM servers WHERE id = 10005")) rows = await _fetch_rows( @@ -350,7 +346,7 @@ async def test_dice_rolls_insert_and_read(db_session): await _execute(db_session, text("INSERT INTO servers (id, name) VALUES (10006, 'Dice Server')")) await _execute( db_session, - text("INSERT INTO dice_rolls (server_id, user_id, roll, dice_size) " "VALUES (10006, 555, 18, 20)"), + text("INSERT INTO dice_rolls (server_id, user_id, roll, dice_size) VALUES (10006, 555, 18, 20)"), ) rows = await _fetch_rows( db_session, diff --git a/tests/integration/test_gw2_session_chars_dal.py b/tests/integration/test_gw2_session_chars_dal.py index b2c1e62d..8f574d5a 100644 --- a/tests/integration/test_gw2_session_chars_dal.py +++ b/tests/integration/test_gw2_session_chars_dal.py @@ -28,9 +28,8 @@ async def _insert_char_directly(db_session, session_id, user_id, name, professio await db_utils.insert(stmt) -async def test_insert_session_char_missing_start_field(db_session, log): - """The DAL's insert_session_char does not set the `start` boolean, which is NOT NULL. - This test documents that the DAL raises IntegrityError against a real DB.""" +async def test_insert_session_char_with_start_field(db_session, log): + """The DAL's insert_session_char now correctly sets start/end booleans.""" sessions_dal = Gw2SessionsDal(db_session, log) chars_dal = Gw2SessionCharsDal(db_session, log) @@ -47,9 +46,20 @@ async def test_insert_session_char_missing_start_field(db_session, log): "profession": "Warrior", "deaths": 10, } - insert_args = {"session_id": session_id, "user_id": USER_ID, "api_key": API_KEY} - with pytest.raises(IntegrityError): - await chars_dal.insert_session_char(gw2_api, ["TestChar"], insert_args) + insert_args = { + "session_id": session_id, + "user_id": USER_ID, + "api_key": API_KEY, + "start": True, + "end": False, + } + # Should no longer raise IntegrityError now that start/end are passed + await chars_dal.insert_session_char(gw2_api, ["TestChar"], insert_args) + + # Verify the data was inserted correctly + results = await chars_dal.get_all_start_characters(USER_ID) + assert isinstance(results, list) + assert len(results) >= 1 async def test_get_all_start_characters(db_session, log): diff --git a/tests/unit/bot/cogs/admin/test_admin.py b/tests/unit/bot/cogs/admin/test_admin.py index a7bf1487..0f335193 100644 --- a/tests/unit/bot/cogs/admin/test_admin.py +++ b/tests/unit/bot/cogs/admin/test_admin.py @@ -7,7 +7,7 @@ from discord.ext import commands from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.admin.admin import Admin from src.bot.constants import messages @@ -69,7 +69,7 @@ def test_init(self, mock_bot): assert cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.admin.admin.bot_utils.invoke_subcommand') + @patch("src.bot.cogs.admin.admin.bot_utils.invoke_subcommand") async def test_admin_group_command(self, mock_invoke, admin_cog, mock_ctx): """Test admin group command.""" mock_invoke.return_value = "mock_command" @@ -80,7 +80,7 @@ async def test_admin_group_command(self, mock_invoke, admin_cog, mock_ctx): assert result == "mock_command" @pytest.mark.asyncio - @patch('src.bot.cogs.admin.admin.bot_utils.send_embed') + @patch("src.bot.cogs.admin.admin.bot_utils.send_embed") async def test_botgame_command_success(self, mock_send_embed, admin_cog, mock_ctx): """Test successful botgame command execution.""" game = "Minecraft" @@ -92,7 +92,7 @@ async def test_botgame_command_success(self, mock_send_embed, admin_cog, mock_ct # Verify bot presence change admin_cog.bot.change_presence.assert_called_once() - activity_call = admin_cog.bot.change_presence.call_args[1]['activity'] + activity_call = admin_cog.bot.change_presence.call_args[1]["activity"] assert isinstance(activity_call, discord.Game) assert activity_call.name == f"{game} | !help" @@ -101,12 +101,12 @@ async def test_botgame_command_success(self, mock_send_embed, admin_cog, mock_ct embed_call = mock_send_embed.call_args_list[0] embed = embed_call[0][1] - assert messages.BOT_ANNOUNCE_PLAYING.format(game) in embed.description + assert messages.bot_announce_playing(game) in embed.description assert embed.author.name == "TestBot" assert embed.author.icon_url == "https://example.com/bot_avatar.png" @pytest.mark.asyncio - @patch('src.bot.cogs.admin.admin.bot_utils.send_embed') + @patch("src.bot.cogs.admin.admin.bot_utils.send_embed") async def test_botgame_command_with_bg_timer_warning(self, mock_send_embed, admin_cog, mock_ctx): """Test botgame command with background activity timer warning.""" game = "Reading documentation" @@ -119,11 +119,11 @@ async def test_botgame_command_with_bg_timer_warning(self, mock_send_embed, admi # Check success embed success_embed = mock_send_embed.call_args_list[0][0][1] - assert messages.BOT_ANNOUNCE_PLAYING.format(game) in success_embed.description + assert messages.bot_announce_playing(game) in success_embed.description # Check warning embed warning_embed = mock_send_embed.call_args_list[1][0][1] - assert messages.BG_TASK_WARNING.format(300) in warning_embed.description + assert messages.bg_task_warning(300) in warning_embed.description # Verify warning embed was sent with dm=False warning_call = mock_send_embed.call_args_list[1] @@ -135,12 +135,12 @@ async def test_warn_about_bg_activity_timer_enabled(self, admin_cog, mock_ctx): """Test _warn_about_bg_activity_timer when timer is enabled.""" admin_cog.bot.settings["bot"]["BGActivityTimer"] = 600 - with patch('src.bot.cogs.admin.admin.bot_utils.send_embed') as mock_send_embed: + with patch("src.bot.cogs.admin.admin.bot_utils.send_embed") as mock_send_embed: await admin_cog._warn_about_bg_activity_timer(mock_ctx) mock_send_embed.assert_called_once() embed = mock_send_embed.call_args[0][1] - assert messages.BG_TASK_WARNING.format(600) in embed.description + assert messages.bg_task_warning(600) in embed.description # Verify dm=False parameter assert mock_send_embed.call_args[0][2] is False @@ -149,7 +149,7 @@ async def test_warn_about_bg_activity_timer_disabled(self, admin_cog, mock_ctx): """Test _warn_about_bg_activity_timer when timer is disabled.""" admin_cog.bot.settings["bot"]["BGActivityTimer"] = 0 - with patch('src.bot.cogs.admin.admin.bot_utils.send_embed') as mock_send_embed: + with patch("src.bot.cogs.admin.admin.bot_utils.send_embed") as mock_send_embed: await admin_cog._warn_about_bg_activity_timer(mock_ctx) mock_send_embed.assert_not_called() @@ -159,7 +159,7 @@ async def test_warn_about_bg_activity_timer_none(self, admin_cog, mock_ctx): """Test _warn_about_bg_activity_timer when timer is None.""" admin_cog.bot.settings["bot"]["BGActivityTimer"] = None - with patch('src.bot.cogs.admin.admin.bot_utils.send_embed') as mock_send_embed: + with patch("src.bot.cogs.admin.admin.bot_utils.send_embed") as mock_send_embed: await admin_cog._warn_about_bg_activity_timer(mock_ctx) mock_send_embed.assert_not_called() @@ -173,7 +173,7 @@ def test_create_admin_embed(self, admin_cog): assert embed.description == description @pytest.mark.asyncio - @patch('src.bot.cogs.admin.admin.bot_utils.send_embed') + @patch("src.bot.cogs.admin.admin.bot_utils.send_embed") async def test_botgame_with_special_characters(self, mock_send_embed, admin_cog, mock_ctx): """Test botgame command with special characters in game name.""" game = "Game with émojis 🎮 and spéciál chars!" @@ -181,7 +181,7 @@ async def test_botgame_with_special_characters(self, mock_send_embed, admin_cog, await admin_cog.botgame.callback(admin_cog, mock_ctx, game=game) # Verify the game name is properly handled - activity_call = admin_cog.bot.change_presence.call_args[1]['activity'] + activity_call = admin_cog.bot.change_presence.call_args[1]["activity"] expected_name = f"{game} | !help" assert activity_call.name == expected_name @@ -190,7 +190,7 @@ async def test_botgame_with_special_characters(self, mock_send_embed, admin_cog, assert game in embed.description @pytest.mark.asyncio - @patch('src.bot.cogs.admin.admin.bot_utils.send_embed') + @patch("src.bot.cogs.admin.admin.bot_utils.send_embed") async def test_botgame_with_empty_game_name(self, mock_send_embed, admin_cog, mock_ctx): """Test botgame command with empty game name.""" game = "" @@ -198,11 +198,11 @@ async def test_botgame_with_empty_game_name(self, mock_send_embed, admin_cog, mo await admin_cog.botgame.callback(admin_cog, mock_ctx, game=game) # Even empty game name should work - activity_call = admin_cog.bot.change_presence.call_args[1]['activity'] + activity_call = admin_cog.bot.change_presence.call_args[1]["activity"] assert activity_call.name == " | !help" @pytest.mark.asyncio - @patch('src.bot.cogs.admin.admin.bot_utils.send_embed') + @patch("src.bot.cogs.admin.admin.bot_utils.send_embed") async def test_botgame_with_no_bot_avatar(self, mock_send_embed, admin_cog, mock_ctx): """Test botgame command when bot has no avatar.""" admin_cog.bot.user.avatar = None @@ -220,10 +220,10 @@ async def test_botgame_different_prefix(self, admin_cog, mock_ctx): admin_cog.bot.command_prefix = ("$",) game = "Test Game" - with patch('src.bot.cogs.admin.admin.bot_utils.send_embed'): + with patch("src.bot.cogs.admin.admin.bot_utils.send_embed"): await admin_cog.botgame.callback(admin_cog, mock_ctx, game=game) - activity_call = admin_cog.bot.change_presence.call_args[1]['activity'] + activity_call = admin_cog.bot.change_presence.call_args[1]["activity"] assert activity_call.name == f"{game} | $help" @pytest.mark.asyncio @@ -239,7 +239,7 @@ async def test_setup_function(self, mock_bot): assert added_cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.admin.admin.bot_utils.send_embed') + @patch("src.bot.cogs.admin.admin.bot_utils.send_embed") async def test_botgame_preserve_embed_formatting(self, mock_send_embed, admin_cog, mock_ctx): """Test that botgame preserves proper embed formatting.""" game = "Markdown **test** `code`" @@ -249,21 +249,21 @@ async def test_botgame_preserve_embed_formatting(self, mock_send_embed, admin_co embed = mock_send_embed.call_args[0][1] # Should be wrapped in code block assert "```" in embed.description - assert messages.BOT_ANNOUNCE_PLAYING.format(game) in embed.description + assert messages.bot_announce_playing(game) in embed.description def test_admin_cog_inheritance(self, admin_cog): """Test that Admin cog properly inherits from commands.Cog.""" assert isinstance(admin_cog, commands.Cog) - assert hasattr(admin_cog, 'bot') + assert hasattr(admin_cog, "bot") @pytest.mark.asyncio async def test_botgame_activity_type(self, admin_cog, mock_ctx): """Test that botgame creates the correct activity type.""" game = "Test Activity" - with patch('src.bot.cogs.admin.admin.bot_utils.send_embed'): + with patch("src.bot.cogs.admin.admin.bot_utils.send_embed"): await admin_cog.botgame.callback(admin_cog, mock_ctx, game=game) - activity_call = admin_cog.bot.change_presence.call_args[1]['activity'] + activity_call = admin_cog.bot.change_presence.call_args[1]["activity"] assert isinstance(activity_call, discord.Game) assert activity_call.type == discord.ActivityType.playing diff --git a/tests/unit/bot/cogs/admin/test_config.py b/tests/unit/bot/cogs/admin/test_config.py index e3e71f01..1469c501 100644 --- a/tests/unit/bot/cogs/admin/test_config.py +++ b/tests/unit/bot/cogs/admin/test_config.py @@ -8,7 +8,7 @@ from discord.ext import commands from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.admin.config import Config, _get_switch_status from src.bot.constants import messages @@ -85,7 +85,7 @@ def test_init(self, mock_bot): assert cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.bot_utils.invoke_subcommand') + @patch("src.bot.cogs.admin.config.bot_utils.invoke_subcommand") async def test_config_group_command(self, mock_invoke, config_cog, mock_ctx): """Test config group command.""" mock_invoke.return_value = "mock_command" @@ -140,8 +140,8 @@ def test_get_switch_status_mixed_case(self): # Test join message configuration @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ServersDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_join_message_on(self, mock_send_embed, mock_dal_class, config_cog, mock_ctx): """Test enabling join messages.""" mock_dal = AsyncMock() @@ -160,8 +160,8 @@ async def test_config_join_message_on(self, mock_send_embed, mock_dal_class, con assert "ON" in embed.description @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ServersDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_join_message_off(self, mock_send_embed, mock_dal_class, config_cog, mock_ctx): """Test disabling join messages.""" mock_dal = AsyncMock() @@ -179,8 +179,8 @@ async def test_config_join_message_off(self, mock_send_embed, mock_dal_class, co # Test leave message configuration @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ServersDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_leave_message_on(self, mock_send_embed, mock_dal_class, config_cog, mock_ctx): """Test enabling leave messages.""" mock_dal = AsyncMock() @@ -196,8 +196,8 @@ async def test_config_leave_message_on(self, mock_send_embed, mock_dal_class, co # Test server message configuration @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ServersDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_server_message_on(self, mock_send_embed, mock_dal_class, config_cog, mock_ctx): """Test enabling server messages.""" mock_dal = AsyncMock() @@ -213,8 +213,8 @@ async def test_config_server_message_on(self, mock_send_embed, mock_dal_class, c # Test member message configuration @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ServersDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_member_message_on(self, mock_send_embed, mock_dal_class, config_cog, mock_ctx): """Test enabling member messages.""" mock_dal = AsyncMock() @@ -230,8 +230,8 @@ async def test_config_member_message_on(self, mock_send_embed, mock_dal_class, c # Test block invisible members configuration @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ServersDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_block_invis_members_on(self, mock_send_embed, mock_dal_class, config_cog, mock_ctx): """Test enabling blocking invisible members.""" mock_dal = AsyncMock() @@ -247,8 +247,8 @@ async def test_config_block_invis_members_on(self, mock_send_embed, mock_dal_cla # Test bot reactions configuration @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ServersDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_bot_word_reactions_on(self, mock_send_embed, mock_dal_class, config_cog, mock_ctx): """Test enabling bot word reactions.""" mock_dal = AsyncMock() @@ -264,8 +264,8 @@ async def test_config_bot_word_reactions_on(self, mock_send_embed, mock_dal_clas # Test profanity filter configuration @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_pfilter_on_success( self, mock_send_embed, mock_dal_class, config_cog, mock_ctx, mock_text_channel ): @@ -288,8 +288,8 @@ async def test_config_pfilter_on_success( assert embed.color == discord.Color.green() @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_pfilter_off_success( self, mock_send_embed, mock_dal_class, config_cog, mock_ctx, mock_text_channel ): @@ -310,7 +310,7 @@ async def test_config_pfilter_off_success( assert embed.color == discord.Color.red() @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.bot_utils.send_error_msg', new_callable=AsyncMock) + @patch("src.bot.cogs.admin.config.bot_utils.send_error_msg", new_callable=AsyncMock) async def test_config_pfilter_missing_arguments(self, mock_send_error, config_cog, mock_ctx): """Test profanity filter with missing arguments.""" from src.bot.cogs.admin.config import config_pfilter @@ -322,7 +322,7 @@ async def test_config_pfilter_missing_arguments(self, mock_send_error, config_co assert messages.MISING_REUIRED_ARGUMENT in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.bot_utils.send_error_msg', new_callable=AsyncMock) + @patch("src.bot.cogs.admin.config.bot_utils.send_error_msg", new_callable=AsyncMock) async def test_config_pfilter_invalid_channel_id(self, mock_send_error, config_cog, mock_ctx): """Test profanity filter with invalid channel ID.""" from src.bot.cogs.admin.config import config_pfilter @@ -334,7 +334,7 @@ async def test_config_pfilter_invalid_channel_id(self, mock_send_error, config_c assert messages.CHANNEL_ID_NOT_FOUND in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.bot_utils.send_error_msg', new_callable=AsyncMock) + @patch("src.bot.cogs.admin.config.bot_utils.send_error_msg", new_callable=AsyncMock) async def test_config_pfilter_channel_not_found(self, mock_send_error, config_cog, mock_ctx): """Test profanity filter with non-existent channel.""" mock_ctx.guild.text_channels = [] @@ -349,7 +349,7 @@ async def test_config_pfilter_channel_not_found(self, mock_send_error, config_co assert messages.CHANNEL_ID_NOT_FOUND in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.config.bot_utils.send_error_msg") async def test_config_pfilter_no_permissions(self, mock_send_error, config_cog, mock_ctx, mock_text_channel): """Test profanity filter without bot permissions.""" mock_ctx.guild.text_channels = [mock_text_channel] @@ -379,11 +379,11 @@ async def test_config_pfilter_invalid_status(self, config_cog, mock_ctx, mock_te # Test config list @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') - @patch('src.bot.cogs.admin.config.chat_formatting.green_text') - @patch('src.bot.cogs.admin.config.chat_formatting.red_text') + @patch("src.bot.cogs.admin.config.ServersDal") + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") + @patch("src.bot.cogs.admin.config.chat_formatting.green_text") + @patch("src.bot.cogs.admin.config.chat_formatting.red_text") async def test_config_list_success( self, mock_red_text, @@ -428,13 +428,13 @@ async def test_config_list_success( # Check the embed sent to DM dm_call_args = mock_ctx.author.send.call_args - embed = dm_call_args[1]['embed'] # embed is passed as keyword argument + embed = dm_call_args[1]["embed"] # embed is passed as keyword argument assert embed.author.name == "Configurations for Test Server" assert len(embed.fields) == 7 # 6 config options + profanity filter @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') + @patch("src.bot.cogs.admin.config.ServersDal") + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") async def test_config_list_no_profanity_channels( self, mock_pf_dal_class, mock_servers_dal_class, config_cog, mock_ctx ): @@ -464,14 +464,14 @@ async def test_config_list_no_profanity_channels( # Check the embed sent to DM dm_call_args = mock_ctx.author.send.call_args - embed = dm_call_args[1]['embed'] # embed is passed as keyword argument + embed = dm_call_args[1]["embed"] # embed is passed as keyword argument # Check that the profanity filter field shows "No channels listed" pf_field = next(field for field in embed.fields if "pfilter" in field.name.lower()) assert messages.NO_CHANNELS_LISTED in pf_field.value @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') + @patch("src.bot.cogs.admin.config.ServersDal") async def test_config_list_no_guild_icon(self, mock_servers_dal_class, config_cog, mock_ctx): """Test listing configurations when guild has no icon.""" mock_ctx.guild.icon = None @@ -487,7 +487,7 @@ async def test_config_list_no_guild_icon(self, mock_servers_dal_class, config_co "bot_word_reactions": False, } - with patch('src.bot.cogs.admin.config.ProfanityFilterDal') as mock_pf_dal_class: + with patch("src.bot.cogs.admin.config.ProfanityFilterDal") as mock_pf_dal_class: mock_pf_dal = AsyncMock() mock_pf_dal_class.return_value = mock_pf_dal mock_pf_dal.get_all_server_profanity_filter_channels.return_value = [] @@ -498,7 +498,7 @@ async def test_config_list_no_guild_icon(self, mock_servers_dal_class, config_co # Check the embed sent to DM dm_call_args = mock_ctx.author.send.call_args - embed = dm_call_args[1]['embed'] # embed is passed as keyword argument + embed = dm_call_args[1]["embed"] # embed is passed as keyword argument assert embed.author.icon_url is None assert embed.thumbnail.url is None @@ -520,11 +520,11 @@ def test_config_cog_inheritance(self, config_cog): from src.bot.cogs.admin.admin import Admin assert isinstance(config_cog, Admin) - assert hasattr(config_cog, 'bot') + assert hasattr(config_cog, "bot") @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_error_msg") async def test_config_pfilter_channel_get_returns_none( self, mock_send_error, mock_dal_class, config_cog, mock_ctx, mock_text_channel ): @@ -542,8 +542,8 @@ async def test_config_pfilter_channel_get_returns_none( assert "98765" in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_pfilter_admin_permission_only( self, mock_send_embed, mock_dal_class, config_cog, mock_ctx, mock_text_channel ): @@ -565,8 +565,8 @@ async def test_config_pfilter_admin_permission_only( mock_send_embed.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_config_pfilter_manage_messages_permission_only( self, mock_send_embed, mock_dal_class, config_cog, mock_ctx, mock_text_channel ): diff --git a/tests/unit/bot/cogs/admin/test_config_extra.py b/tests/unit/bot/cogs/admin/test_config_extra.py index f4e8d147..aa3d5633 100644 --- a/tests/unit/bot/cogs/admin/test_config_extra.py +++ b/tests/unit/bot/cogs/admin/test_config_extra.py @@ -9,7 +9,7 @@ from discord.ext import commands from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.admin.config import Config, ConfigView, _get_switch_status from src.bot.constants import messages @@ -127,8 +127,8 @@ class TestConfigPfilterChannelResolution: """Tests for config_pfilter channel resolution (around line 208).""" @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_pfilter_no_channel_specified_uses_current( self, mock_send_embed, mock_dal_class, config_cog, mock_ctx ): @@ -144,8 +144,8 @@ async def test_pfilter_no_channel_specified_uses_current( mock_send_embed.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_pfilter_channel_found_by_numeric_id( self, mock_send_embed, mock_dal_class, config_cog, mock_ctx, mock_text_channel ): @@ -162,8 +162,8 @@ async def test_pfilter_channel_found_by_numeric_id( mock_dal.insert_profanity_filter_channel.assert_called_once_with(99999, 22222, "general", 12345) @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_pfilter_channel_found_by_name( self, mock_send_embed, mock_dal_class, config_cog, mock_ctx, mock_text_channel ): @@ -174,7 +174,7 @@ async def test_pfilter_channel_found_by_name( mock_dal = AsyncMock() mock_dal_class.return_value = mock_dal - with patch('src.bot.cogs.admin.config.discord.utils.get', return_value=mock_text_channel): + with patch("src.bot.cogs.admin.config.discord.utils.get", return_value=mock_text_channel): from src.bot.cogs.admin.config import config_pfilter await config_pfilter(mock_ctx, subcommand_passed="on general") @@ -182,12 +182,12 @@ async def test_pfilter_channel_found_by_name( mock_dal.insert_profanity_filter_channel.assert_called_once_with(99999, 22222, "general", 12345) @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.bot_utils.send_error_msg', new_callable=AsyncMock) + @patch("src.bot.cogs.admin.config.bot_utils.send_error_msg", new_callable=AsyncMock) async def test_pfilter_channel_not_found_by_id_or_name(self, mock_send_error, config_cog, mock_ctx): """When channel is not found by ID or name, sends error (lines 221-224).""" mock_ctx.guild.get_channel.return_value = None - with patch('src.bot.cogs.admin.config.discord.utils.get', return_value=None): + with patch("src.bot.cogs.admin.config.discord.utils.get", return_value=None): from src.bot.cogs.admin.config import config_pfilter await config_pfilter(mock_ctx, subcommand_passed="on nonexistent") @@ -198,7 +198,7 @@ async def test_pfilter_channel_not_found_by_id_or_name(self, mock_send_error, co assert "nonexistent" in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.bot_utils.send_error_msg', new_callable=AsyncMock) + @patch("src.bot.cogs.admin.config.bot_utils.send_error_msg", new_callable=AsyncMock) async def test_pfilter_on_no_bot_permissions(self, mock_send_error, config_cog, mock_ctx): """When 'on' status with insufficient bot permissions, sends error (lines 231-233).""" mock_ctx.guild.me.guild_permissions.administrator = False @@ -214,8 +214,8 @@ async def test_pfilter_on_no_bot_permissions(self, mock_send_error, config_cog, assert messages.BOT_MISSING_MANAGE_MESSAGES_PERMISSION in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_pfilter_off_deletes(self, mock_send_embed, mock_dal_class, config_cog, mock_ctx): """When 'off' status, deletes channel filter (lines 240-243).""" mock_dal = AsyncMock() @@ -238,8 +238,8 @@ async def test_pfilter_invalid_status_raises(self, config_cog, mock_ctx): await config_pfilter(mock_ctx, subcommand_passed="maybe") @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_pfilter_on_uppercase(self, mock_send_embed, mock_dal_class, config_cog, mock_ctx): """When 'ON' (uppercase) status, inserts channel filter (line 229).""" mock_dal = AsyncMock() @@ -252,8 +252,8 @@ async def test_pfilter_on_uppercase(self, mock_send_embed, mock_dal_class, confi mock_dal.insert_profanity_filter_channel.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') - @patch('src.bot.cogs.admin.config.bot_utils.send_embed') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") + @patch("src.bot.cogs.admin.config.bot_utils.send_embed") async def test_pfilter_off_uppercase(self, mock_send_embed, mock_dal_class, config_cog, mock_ctx): """When 'OFF' (uppercase) status, deletes channel filter (line 240).""" mock_dal = AsyncMock() @@ -304,7 +304,7 @@ async def test_handle_update_spam_clicking_wait_message(self, config_view, mock_ assert "Please wait" in interaction.response.send_message.call_args[0][0] @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') + @patch("src.bot.cogs.admin.config.ServersDal") async def test_handle_update_successful(self, mock_dal_class, config_view, mock_ctx, server_config): """When update succeeds, updates config and edits message (lines 414-458).""" mock_dal = AsyncMock() @@ -329,7 +329,7 @@ async def test_handle_update_successful(self, mock_dal_class, config_view, mock_ mock_child = MagicMock() mock_child.disabled = False mock_child.style = discord.ButtonStyle.success - with patch.object(type(config_view), 'children', new_callable=PropertyMock, return_value=[mock_child]): + with patch.object(type(config_view), "children", new_callable=PropertyMock, return_value=[mock_child]): await config_view._handle_update(interaction, button, "msg_on_join", update_method, "Join Messages") interaction.response.defer.assert_called_once() @@ -344,7 +344,7 @@ async def test_handle_update_successful(self, mock_dal_class, config_view, mock_ assert config_view._updating is False @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') + @patch("src.bot.cogs.admin.config.ServersDal") async def test_handle_update_exception_error_message(self, mock_dal_class, config_view, mock_ctx, server_config): """When update raises exception, sends error message (lines 461-464).""" interaction = MagicMock() @@ -363,17 +363,17 @@ async def test_handle_update_exception_error_message(self, mock_dal_class, confi mock_child = MagicMock() mock_child.disabled = False mock_child.style = discord.ButtonStyle.success - with patch.object(type(config_view), 'children', new_callable=PropertyMock, return_value=[mock_child]): + with patch.object(type(config_view), "children", new_callable=PropertyMock, return_value=[mock_child]): await config_view._handle_update(interaction, button, "msg_on_join", update_method, "Join Messages") # Should have error response last_call = interaction.edit_original_response.call_args_list[-1] - assert "Error updating configuration" in last_call[1]['content'] + assert "Error updating configuration" in last_call[1]["content"] # _updating should be reset to False in finally block assert config_view._updating is False @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') + @patch("src.bot.cogs.admin.config.ServersDal") async def test_handle_update_toggles_from_false_to_true(self, mock_dal_class, config_view, mock_ctx, server_config): """When toggling from False to True, button becomes success style (lines 447-448).""" mock_dal = AsyncMock() @@ -398,14 +398,14 @@ async def test_handle_update_toggles_from_false_to_true(self, mock_dal_class, co mock_child = MagicMock() mock_child.disabled = False mock_child.style = discord.ButtonStyle.success - with patch.object(type(config_view), 'children', new_callable=PropertyMock, return_value=[mock_child]): + with patch.object(type(config_view), "children", new_callable=PropertyMock, return_value=[mock_child]): await config_view._handle_update(interaction, button, "msg_on_leave", update_method, "Leave Messages") assert server_config["msg_on_leave"] is True assert button.style == discord.ButtonStyle.success @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ServersDal') + @patch("src.bot.cogs.admin.config.ServersDal") async def test_handle_update_disables_buttons_during_processing( self, mock_dal_class, config_view, mock_ctx, server_config ): @@ -431,7 +431,7 @@ async def test_handle_update_disables_buttons_during_processing( mock_child.disabled = False mock_child.style = discord.ButtonStyle.success - with patch.object(type(config_view), 'children', new_callable=PropertyMock, return_value=[mock_child]): + with patch.object(type(config_view), "children", new_callable=PropertyMock, return_value=[mock_child]): await config_view._handle_update(interaction, button, "msg_on_join", update_method, "Join Messages") # After completion, _updating should be False @@ -453,7 +453,7 @@ async def test_restore_buttons_sets_correct_styles(self, config_view, server_con # Use property mock for children iteration with patch.object( - type(config_view), 'children', new_callable=PropertyMock, return_value=[mock_child1, mock_child2] + type(config_view), "children", new_callable=PropertyMock, return_value=[mock_child1, mock_child2] ): await config_view._restore_buttons() @@ -480,7 +480,7 @@ async def test_restore_buttons_enables_all_children(self, config_view, server_co mock_child2.disabled = True with patch.object( - type(config_view), 'children', new_callable=PropertyMock, return_value=[mock_child1, mock_child2] + type(config_view), "children", new_callable=PropertyMock, return_value=[mock_child1, mock_child2] ): await config_view._restore_buttons() @@ -492,7 +492,7 @@ class TestConfigViewCreateUpdatedEmbed: """Tests for ConfigView._create_updated_embed (lines 494-559).""" @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") async def test_create_updated_embed_with_pfilter_channels(self, mock_dal_class, config_view, server_config): """Creates embed with profanity filter channels listed (lines 504-506).""" mock_dal = AsyncMock() @@ -513,7 +513,7 @@ async def test_create_updated_embed_with_pfilter_channels(self, mock_dal_class, assert "random" in pf_field.value @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") async def test_create_updated_embed_without_pfilter_channels(self, mock_dal_class, config_view, server_config): """Creates embed without profanity filter channels (lines 507-508).""" mock_dal = AsyncMock() @@ -527,7 +527,7 @@ async def test_create_updated_embed_without_pfilter_channels(self, mock_dal_clas assert messages.NO_CHANNELS_LISTED in pf_field.value @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") async def test_create_updated_embed_status_indicators(self, mock_dal_class, config_view, server_config): """Status indicators match config (lines 510-512, 522-556).""" mock_dal = AsyncMock() @@ -544,7 +544,7 @@ async def test_create_updated_embed_status_indicators(self, mock_dal_class, conf assert "OFF" in leave_field.value @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") async def test_create_updated_embed_no_guild_icon(self, mock_dal_class, mock_ctx, server_config): """Handles no guild icon gracefully (line 518).""" mock_ctx.guild.icon = None @@ -566,7 +566,7 @@ async def test_create_updated_embed_no_guild_icon(self, mock_dal_class, mock_ctx assert embed.thumbnail.url is None @pytest.mark.asyncio - @patch('src.bot.cogs.admin.config.ProfanityFilterDal') + @patch("src.bot.cogs.admin.config.ProfanityFilterDal") async def test_create_updated_embed_footer(self, mock_dal_class, config_view, server_config): """Embed footer contains help info (line 558).""" mock_dal = AsyncMock() @@ -588,7 +588,7 @@ class TestConfigViewButtonCallbacks: def _get_real_config_view(self, mock_ctx, server_config): """Create a ConfigView with real button methods but no event loop dependency.""" - with patch.object(discord.ui.View, '__init__', lambda self, **kwargs: None): + with patch.object(discord.ui.View, "__init__", lambda self, **kwargs: None): view = object.__new__(ConfigView) view.ctx = mock_ctx view.server_config = server_config @@ -714,7 +714,7 @@ async def test_on_timeout_disables_buttons(self, config_view): mock_child2.disabled = False with patch.object( - type(config_view), 'children', new_callable=PropertyMock, return_value=[mock_child1, mock_child2] + type(config_view), "children", new_callable=PropertyMock, return_value=[mock_child1, mock_child2] ): await config_view.on_timeout() @@ -732,7 +732,7 @@ async def test_on_timeout_message_not_found(self, config_view): mock_child = MagicMock() mock_child.disabled = False - with patch.object(type(config_view), 'children', new_callable=PropertyMock, return_value=[mock_child]): + with patch.object(type(config_view), "children", new_callable=PropertyMock, return_value=[mock_child]): # Should not raise await config_view.on_timeout() @@ -748,7 +748,7 @@ async def test_on_timeout_http_exception(self, config_view): mock_child = MagicMock() mock_child.disabled = False - with patch.object(type(config_view), 'children', new_callable=PropertyMock, return_value=[mock_child]): + with patch.object(type(config_view), "children", new_callable=PropertyMock, return_value=[mock_child]): # Should not raise await config_view.on_timeout() @@ -815,7 +815,7 @@ class TestConfigViewButtonUpdateMethodLambdas: def _get_real_config_view(self, mock_ctx, server_config): """Create a ConfigView with real button methods but no event loop dependency.""" - with patch.object(discord.ui.View, '__init__', lambda self, **kwargs: None): + with patch.object(discord.ui.View, "__init__", lambda self, **kwargs: None): view = object.__new__(ConfigView) view.ctx = mock_ctx view.server_config = server_config diff --git a/tests/unit/bot/cogs/admin/test_custom_cmd.py b/tests/unit/bot/cogs/admin/test_custom_cmd.py index a84b6681..808df24f 100644 --- a/tests/unit/bot/cogs/admin/test_custom_cmd.py +++ b/tests/unit/bot/cogs/admin/test_custom_cmd.py @@ -7,7 +7,7 @@ from datetime import datetime from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.admin.custom_cmd import CustomCommand from src.bot.constants import messages @@ -78,7 +78,7 @@ def test_init(self, mock_bot): assert cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.invoke_subcommand') + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.invoke_subcommand") async def test_custom_command_group(self, mock_invoke, custom_cmd_cog, mock_ctx): """Test custom_command group command.""" mock_invoke.return_value = "mock_command" @@ -92,9 +92,9 @@ async def test_custom_command_group(self, mock_invoke, custom_cmd_cog, mock_ctx) # Test add command @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_msg") async def test_add_custom_command_success( self, mock_send_msg, mock_delete_msg, mock_dal_class, custom_cmd_cog, mock_ctx ): @@ -117,8 +117,8 @@ async def test_add_custom_command_success( assert "!hello" in success_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") async def test_add_custom_command_missing_args(self, mock_send_error, mock_delete_msg, custom_cmd_cog, mock_ctx): """Test add command with missing arguments.""" from src.bot.cogs.admin.custom_cmd import add_custom_command @@ -132,8 +132,8 @@ async def test_add_custom_command_missing_args(self, mock_send_error, mock_delet assert messages.MISSING_REQUIRED_ARGUMENT_HELP_MESSAGE in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") async def test_add_custom_command_name_too_long(self, mock_send_error, mock_delete_msg, custom_cmd_cog, mock_ctx): """Test add command with name too long.""" long_name = "a" * 21 # 21 characters, exceeds limit of 20 @@ -146,8 +146,8 @@ async def test_add_custom_command_name_too_long(self, mock_send_error, mock_dele mock_send_error.assert_called_once_with(mock_ctx, messages.COMMAND_LENGHT_ERROR) @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") async def test_add_custom_command_conflicts_with_bot_command( self, mock_send_error, mock_delete_msg, custom_cmd_cog, mock_ctx ): @@ -164,9 +164,9 @@ async def test_add_custom_command_conflicts_with_bot_command( assert "!help" in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_warning_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_warning_msg") async def test_add_custom_command_already_exists( self, mock_send_warning, mock_delete_msg, mock_dal_class, custom_cmd_cog, mock_ctx ): @@ -189,9 +189,9 @@ async def test_add_custom_command_already_exists( # Test edit command @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_msg") async def test_edit_custom_command_success( self, mock_send_msg, mock_delete_msg, mock_dal_class, custom_cmd_cog, mock_ctx, mock_command_data ): @@ -214,8 +214,8 @@ async def test_edit_custom_command_success( assert "!hello" in success_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") async def test_edit_custom_command_missing_args(self, mock_send_error, mock_delete_msg, custom_cmd_cog, mock_ctx): """Test edit command with missing arguments.""" from src.bot.cogs.admin.custom_cmd import edit_custom_command @@ -229,9 +229,9 @@ async def test_edit_custom_command_missing_args(self, mock_send_error, mock_dele assert messages.MISSING_REQUIRED_ARGUMENT_HELP_MESSAGE in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") async def test_edit_custom_command_no_commands_exist( self, mock_send_error, mock_delete_msg, mock_dal_class, custom_cmd_cog, mock_ctx ): @@ -248,9 +248,9 @@ async def test_edit_custom_command_no_commands_exist( mock_send_error.assert_called_once_with(mock_ctx, messages.NO_CUSTOM_COMMANDS_FOUND) @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") async def test_edit_custom_command_not_found( self, mock_send_error, mock_delete_msg, mock_dal_class, custom_cmd_cog, mock_ctx, mock_command_data ): @@ -272,8 +272,8 @@ async def test_edit_custom_command_not_found( # Test remove command @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_msg") async def test_remove_custom_command_success( self, mock_send_msg, mock_dal_class, custom_cmd_cog, mock_ctx, mock_command_data ): @@ -295,8 +295,8 @@ async def test_remove_custom_command_success( assert "!hello" in success_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_warning_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_warning_msg") async def test_remove_custom_command_no_commands_exist( self, mock_send_warning, mock_dal_class, custom_cmd_cog, mock_ctx ): @@ -312,8 +312,8 @@ async def test_remove_custom_command_no_commands_exist( mock_send_warning.assert_called_once_with(mock_ctx, messages.NO_CUSTOM_COMMANDS_FOUND) @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") async def test_remove_custom_command_not_found( self, mock_send_error, mock_dal_class, custom_cmd_cog, mock_ctx, mock_command_data ): @@ -335,8 +335,8 @@ async def test_remove_custom_command_not_found( # Test removeall command @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_msg") async def test_remove_all_custom_commands_success( self, mock_send_msg, mock_dal_class, custom_cmd_cog, mock_ctx, mock_command_data ): @@ -354,8 +354,8 @@ async def test_remove_all_custom_commands_success( mock_send_msg.assert_called_once_with(mock_ctx, messages.CUSTOM_COMMAND_ALL_REMOVED) @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") async def test_remove_all_custom_commands_no_commands_exist( self, mock_send_error, mock_dal_class, custom_cmd_cog, mock_ctx ): @@ -372,11 +372,11 @@ async def test_remove_all_custom_commands_no_commands_exist( # Test list command @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_embed') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.get_member_by_id') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.convert_datetime_to_str_short') - @patch('src.bot.cogs.admin.custom_cmd.chat_formatting.inline') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_embed") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.get_member_by_id") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.convert_datetime_to_str_short") + @patch("src.bot.cogs.admin.custom_cmd.chat_formatting.inline") async def test_list_custom_commands_success( self, mock_inline, @@ -420,8 +420,8 @@ async def test_list_custom_commands_success( assert embed.footer.text == "For more info: !help admin cc" @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_warning_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_warning_msg") async def test_list_custom_commands_no_commands_exist( self, mock_send_warning, mock_dal_class, custom_cmd_cog, mock_ctx ): @@ -437,11 +437,11 @@ async def test_list_custom_commands_no_commands_exist( mock_send_warning.assert_called_once_with(mock_ctx, messages.NO_CUSTOM_COMMANDS_FOUND) @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_embed') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.get_member_by_id') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.convert_datetime_to_str_short') - @patch('src.bot.cogs.admin.custom_cmd.chat_formatting.inline') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_embed") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.get_member_by_id") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.convert_datetime_to_str_short") + @patch("src.bot.cogs.admin.custom_cmd.chat_formatting.inline") async def test_list_custom_commands_unknown_user( self, mock_inline, @@ -475,11 +475,11 @@ async def test_list_custom_commands_unknown_user( assert "Unknown User" in created_by_field.value @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_embed') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.get_member_by_id') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.convert_datetime_to_str_short') - @patch('src.bot.cogs.admin.custom_cmd.chat_formatting.inline') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_embed") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.get_member_by_id") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.convert_datetime_to_str_short") + @patch("src.bot.cogs.admin.custom_cmd.chat_formatting.inline") async def test_list_custom_commands_no_guild_icon( self, mock_inline, @@ -531,12 +531,12 @@ def test_custom_command_cog_inheritance(self, custom_cmd_cog): from src.bot.cogs.admin.admin import Admin assert isinstance(custom_cmd_cog, Admin) - assert hasattr(custom_cmd_cog, 'bot') + assert hasattr(custom_cmd_cog, "bot") @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_msg") async def test_add_custom_command_case_insensitive_conflict( self, mock_send_msg, mock_delete_msg, mock_dal_class, custom_cmd_cog, mock_ctx ): @@ -548,7 +548,7 @@ async def test_add_custom_command_case_insensitive_conflict( from src.bot.cogs.admin.custom_cmd import add_custom_command - with patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') as mock_send_error: + with patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") as mock_send_error: await add_custom_command(mock_ctx, subcommand_passed="help Some description") mock_send_error.assert_called_once() @@ -556,9 +556,9 @@ async def test_add_custom_command_case_insensitive_conflict( assert messages.ALREADY_A_STANDARD_COMMAND in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_msg") async def test_add_custom_command_with_spaces_in_description( self, mock_send_msg, mock_delete_msg, mock_dal_class, custom_cmd_cog, mock_ctx ): @@ -576,9 +576,9 @@ async def test_add_custom_command_with_spaces_in_description( ) @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.CustomCommandsDal') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_msg') + @patch("src.bot.cogs.admin.custom_cmd.CustomCommandsDal") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_msg") async def test_add_custom_command_exact_20_chars( self, mock_send_msg, mock_delete_msg, mock_dal_class, custom_cmd_cog, mock_ctx ): @@ -597,8 +597,8 @@ async def test_add_custom_command_exact_20_chars( mock_send_msg.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") async def test_add_custom_command_empty_args(self, mock_send_error, mock_delete_msg, custom_cmd_cog, mock_ctx): """Test add command with completely empty arguments.""" from src.bot.cogs.admin.custom_cmd import add_custom_command @@ -609,8 +609,8 @@ async def test_add_custom_command_empty_args(self, mock_send_error, mock_delete_ mock_send_error.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.delete_message') - @patch('src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg') + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.delete_message") + @patch("src.bot.cogs.admin.custom_cmd.bot_utils.send_error_msg") async def test_edit_custom_command_empty_args(self, mock_send_error, mock_delete_msg, custom_cmd_cog, mock_ctx): """Test edit command with completely empty arguments.""" from src.bot.cogs.admin.custom_cmd import edit_custom_command diff --git a/tests/unit/bot/cogs/test_dice_rolls.py b/tests/unit/bot/cogs/test_dice_rolls.py index 5ac420df..7a129b38 100644 --- a/tests/unit/bot/cogs/test_dice_rolls.py +++ b/tests/unit/bot/cogs/test_dice_rolls.py @@ -5,7 +5,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.dice_rolls import DiceRolls from src.bot.constants import messages @@ -54,16 +54,15 @@ def mock_ctx(): class TestDiceRolls: - @pytest.mark.asyncio async def test_init(self, mock_bot): cog = DiceRolls(mock_bot) assert cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.random.SystemRandom') - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') + @patch("src.bot.cogs.dice_rolls.random.SystemRandom") + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") async def test_roll_default_dice_size(self, mock_send_embed, mock_dal_class, mock_random, dice_cog, mock_ctx): # Setup mock_random_instance = MagicMock() @@ -87,9 +86,9 @@ async def test_roll_default_dice_size(self, mock_send_embed, mock_dal_class, moc mock_send_embed.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.random.SystemRandom') - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') + @patch("src.bot.cogs.dice_rolls.random.SystemRandom") + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") async def test_roll_custom_dice_size(self, mock_send_embed, mock_dal_class, mock_random, dice_cog, mock_ctx): # Setup mock_ctx.subcommand_passed = "20" @@ -112,7 +111,7 @@ async def test_roll_custom_dice_size(self, mock_send_embed, mock_dal_class, mock mock_dal.insert_user_roll.assert_called_once_with(12345, 67890, 20, 15) @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.bot_utils.send_error_msg') + @patch("src.bot.cogs.dice_rolls.bot_utils.send_error_msg") async def test_roll_invalid_dice_size(self, mock_send_error, dice_cog, mock_ctx): # Setup mock_ctx.subcommand_passed = "invalid" @@ -124,7 +123,7 @@ async def test_roll_invalid_dice_size(self, mock_send_error, dice_cog, mock_ctx) mock_send_error.assert_called_once_with(mock_ctx, messages.DICE_SIZE_NOT_VALID) @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.bot_utils.send_error_msg') + @patch("src.bot.cogs.dice_rolls.bot_utils.send_error_msg") async def test_roll_dice_size_too_small(self, mock_send_error, dice_cog, mock_ctx): # Setup mock_ctx.subcommand_passed = "1" @@ -136,10 +135,10 @@ async def test_roll_dice_size_too_small(self, mock_send_error, dice_cog, mock_ct mock_send_error.assert_called_once_with(mock_ctx, messages.DICE_SIZE_HIGHER_ONE) @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.random.SystemRandom') - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') - @patch('src.bot.cogs.dice_rolls.bot_utils.get_member_by_id') + @patch("src.bot.cogs.dice_rolls.random.SystemRandom") + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") + @patch("src.bot.cogs.dice_rolls.bot_utils.get_member_by_id") async def test_roll_new_personal_record( self, mock_get_member, mock_send_embed, mock_dal_class, mock_random, dice_cog, mock_ctx ): @@ -173,10 +172,10 @@ async def test_roll_new_personal_record( assert messages.MEMBER_HIGHEST_ROLL in embed_call.description @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.random.SystemRandom') - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') - @patch('src.bot.cogs.dice_rolls.bot_utils.get_member_by_id') + @patch("src.bot.cogs.dice_rolls.random.SystemRandom") + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") + @patch("src.bot.cogs.dice_rolls.bot_utils.get_member_by_id") async def test_roll_new_server_record( self, mock_get_member, mock_send_embed, mock_dal_class, mock_random, dice_cog, mock_ctx ): @@ -215,9 +214,9 @@ async def test_roll_with_invoked_subcommand(self, dice_cog, mock_ctx): mock_ctx.message.channel.typing.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') - @patch('src.bot.cogs.dice_rolls.bot_utils.get_member_by_id') + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") + @patch("src.bot.cogs.dice_rolls.bot_utils.get_member_by_id") async def test_roll_results_default_dice_size( self, mock_get_member, mock_send_embed, mock_dal_class, dice_cog, mock_ctx ): @@ -243,9 +242,9 @@ async def test_roll_results_default_dice_size( mock_send_embed.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') - @patch('src.bot.cogs.dice_rolls.bot_utils.get_member_by_id') + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") + @patch("src.bot.cogs.dice_rolls.bot_utils.get_member_by_id") async def test_roll_results_custom_dice_size( self, mock_get_member, mock_send_embed, mock_dal_class, dice_cog, mock_ctx ): @@ -268,8 +267,8 @@ async def test_roll_results_custom_dice_size( mock_dal.get_all_server_rolls.assert_called_once_with(12345, 20) @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_error_msg') + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_error_msg") async def test_roll_results_no_rolls_found(self, mock_send_error, mock_dal_class, dice_cog, mock_ctx): # Setup mock_dal = AsyncMock() @@ -280,10 +279,10 @@ async def test_roll_results_no_rolls_found(self, mock_send_error, mock_dal_class await dice_cog.roll_results.callback(dice_cog, mock_ctx) # Verify - mock_send_error.assert_called_once_with(mock_ctx, messages.NO_DICE_SIZE_ROLLS.format(100)) + mock_send_error.assert_called_once_with(mock_ctx, messages.no_dice_size_rolls(100)) @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.bot_utils.send_error_msg') + @patch("src.bot.cogs.dice_rolls.bot_utils.send_error_msg") async def test_roll_results_invalid_dice_size(self, mock_send_error, dice_cog, mock_ctx): # Setup mock_ctx.message.content = "!roll results invalid" @@ -295,8 +294,8 @@ async def test_roll_results_invalid_dice_size(self, mock_send_error, dice_cog, m mock_send_error.assert_called_once_with(mock_ctx, messages.DICE_SIZE_NOT_VALID) @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_msg') + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_msg") async def test_roll_reset(self, mock_send_msg, mock_dal_class, dice_cog, mock_ctx): # Setup mock_dal = AsyncMock() @@ -312,10 +311,10 @@ async def test_roll_reset(self, mock_send_msg, mock_dal_class, dice_cog, mock_ct mock_send_msg.assert_called_once_with(mock_ctx, messages.DELETED_ALL_ROLLS) @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.random.SystemRandom') - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') - @patch('src.bot.cogs.dice_rolls.bot_utils.get_member_by_id') + @patch("src.bot.cogs.dice_rolls.random.SystemRandom") + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") + @patch("src.bot.cogs.dice_rolls.bot_utils.get_member_by_id") async def test_roll_existing_user_no_new_record( self, mock_get_member, mock_send_embed, mock_dal_class, mock_random, dice_cog, mock_ctx ): @@ -337,10 +336,10 @@ async def test_roll_existing_user_no_new_record( mock_dal.update_user_roll.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.random.SystemRandom') - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') - @patch('src.bot.cogs.dice_rolls.bot_utils.get_member_by_id') + @patch("src.bot.cogs.dice_rolls.random.SystemRandom") + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") + @patch("src.bot.cogs.dice_rolls.bot_utils.get_member_by_id") async def test_roll_server_highest_user_is_current_user( self, mock_get_member, mock_send_embed, mock_dal_class, mock_random, dice_cog, mock_ctx ): @@ -365,10 +364,10 @@ async def test_roll_server_highest_user_is_current_user( assert messages.MEMBER_SERVER_WINNER_ANOUNCE in embed_call.description @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.random.SystemRandom') - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') - @patch('src.bot.cogs.dice_rolls.bot_utils.get_member_by_id') + @patch("src.bot.cogs.dice_rolls.random.SystemRandom") + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") + @patch("src.bot.cogs.dice_rolls.bot_utils.get_member_by_id") async def test_roll_server_highest_user_is_different_user( self, mock_get_member, mock_send_embed, mock_dal_class, mock_random, dice_cog, mock_ctx ): @@ -396,9 +395,9 @@ async def test_roll_server_highest_user_is_different_user( assert messages.MEMBER_HIGHEST_ROLL in embed_call.description @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.random.SystemRandom') - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') + @patch("src.bot.cogs.dice_rolls.random.SystemRandom") + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") async def test_roll_server_max_roll_with_null_max_roll( self, mock_send_embed, mock_dal_class, mock_random, dice_cog, mock_ctx ): @@ -434,10 +433,10 @@ async def test_setup_function(self, mock_bot): assert added_cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') - @patch('src.bot.cogs.dice_rolls.chat_formatting.inline') - @patch('src.bot.cogs.dice_rolls.bot_utils.get_member_by_id') + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") + @patch("src.bot.cogs.dice_rolls.chat_formatting.inline") + @patch("src.bot.cogs.dice_rolls.bot_utils.get_member_by_id") async def test_roll_results_embed_formatting( self, mock_get_member, mock_inline, mock_send_embed, mock_dal_class, dice_cog, mock_ctx ): @@ -474,9 +473,9 @@ async def test_roll_results_embed_formatting( assert embed.footer.text == f"{messages.RESET_ALL_ROLLS}: !roll reset" @pytest.mark.asyncio - @patch('src.bot.cogs.dice_rolls.random.SystemRandom') - @patch('src.bot.cogs.dice_rolls.DiceRollsDal') - @patch('src.bot.cogs.dice_rolls.bot_utils.send_embed') + @patch("src.bot.cogs.dice_rolls.random.SystemRandom") + @patch("src.bot.cogs.dice_rolls.DiceRollsDal") + @patch("src.bot.cogs.dice_rolls.bot_utils.send_embed") async def test_roll_embed_properties(self, mock_send_embed, mock_dal_class, mock_random, dice_cog, mock_ctx): # Setup mock_random_instance = MagicMock() diff --git a/tests/unit/bot/cogs/test_misc.py b/tests/unit/bot/cogs/test_misc.py index 3331eaf7..b3109f1c 100644 --- a/tests/unit/bot/cogs/test_misc.py +++ b/tests/unit/bot/cogs/test_misc.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch # Mock problematic imports before importing the module -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.misc import Misc from src.bot.constants import messages, variables @@ -107,20 +107,20 @@ def test_init(self, mock_bot): """Test Misc cog initialization.""" cog = Misc(mock_bot) assert cog.bot == mock_bot - assert hasattr(cog, '_random') + assert hasattr(cog, "_random") # Test pepe command @pytest.mark.asyncio - @patch('src.bot.cogs.misc.pepedatabase', ['https://example.com/pepe1.jpg', 'https://example.com/pepe2.jpg']) + @patch("src.bot.cogs.misc.pepedatabase", ["https://example.com/pepe1.jpg", "https://example.com/pepe2.jpg"]) async def test_pepe_command_success(self, misc_cog, mock_ctx): """Test successful pepe command execution.""" mock_ctx.subcommand_passed = None - with patch.object(misc_cog._random, 'choice', return_value='https://example.com/pepe1.jpg'): + with patch.object(misc_cog._random, "choice", return_value="https://example.com/pepe1.jpg"): await misc_cog.pepe.callback(misc_cog, mock_ctx) mock_ctx.message.channel.typing.assert_called_once() - mock_ctx.send.assert_called_once_with('https://example.com/pepe1.jpg') + mock_ctx.send.assert_called_once_with("https://example.com/pepe1.jpg") @pytest.mark.asyncio async def test_pepe_command_with_subcommand(self, misc_cog, mock_ctx): @@ -132,8 +132,8 @@ async def test_pepe_command_with_subcommand(self, misc_cog, mock_ctx): # Test TTS command @pytest.mark.asyncio - @patch('src.bot.cogs.misc.gTTS') - @patch('src.bot.cogs.misc.bot_utils.send_error_msg') + @patch("src.bot.cogs.misc.gTTS") + @patch("src.bot.cogs.misc.bot_utils.send_error_msg") async def test_tts_command_success(self, mock_send_error, mock_gtts_class, misc_cog, mock_ctx): """Test successful TTS command execution.""" # Setup TTS mock @@ -148,13 +148,13 @@ async def test_tts_command_success(self, mock_send_error, mock_gtts_class, misc_ mock_ctx.send.assert_called_once() # Verify file was sent - sent_file = mock_ctx.send.call_args[1]['file'] + sent_file = mock_ctx.send.call_args[1]["file"] assert isinstance(sent_file, discord.File) assert sent_file.filename == "TestUser.mp3" @pytest.mark.asyncio - @patch('src.bot.cogs.misc.gTTS') - @patch('src.bot.cogs.misc.bot_utils.send_error_msg') + @patch("src.bot.cogs.misc.gTTS") + @patch("src.bot.cogs.misc.bot_utils.send_error_msg") async def test_tts_command_with_mentions(self, mock_send_error, mock_gtts_class, misc_cog, mock_ctx): """Test TTS command with user mentions.""" # Setup mock member @@ -169,12 +169,12 @@ async def test_tts_command_with_mentions(self, mock_send_error, mock_gtts_class, # Should process mention and call gTTS with processed text mock_gtts_class.assert_called_once() - processed_text = mock_gtts_class.call_args[1]['text'] + processed_text = mock_gtts_class.call_args[1]["text"] assert "@MentionedUser" in processed_text @pytest.mark.asyncio - @patch('src.bot.cogs.misc.gTTS') - @patch('src.bot.cogs.misc.bot_utils.send_error_msg') + @patch("src.bot.cogs.misc.gTTS") + @patch("src.bot.cogs.misc.bot_utils.send_error_msg") async def test_tts_command_with_emojis(self, mock_send_error, mock_gtts_class, misc_cog, mock_ctx): """Test TTS command with custom emojis.""" mock_tts = MagicMock() @@ -184,12 +184,12 @@ async def test_tts_command_with_emojis(self, mock_send_error, mock_gtts_class, m # Should process emoji and call gTTS with processed text mock_gtts_class.assert_called_once() - processed_text = mock_gtts_class.call_args[1]['text'] + processed_text = mock_gtts_class.call_args[1]["text"] assert "smile" in processed_text @pytest.mark.asyncio - @patch('src.bot.cogs.misc.gTTS', side_effect=AssertionError("TTS Error")) - @patch('src.bot.cogs.misc.bot_utils.send_error_msg') + @patch("src.bot.cogs.misc.gTTS", side_effect=AssertionError("TTS Error")) + @patch("src.bot.cogs.misc.bot_utils.send_error_msg") async def test_tts_command_error(self, mock_send_error, mock_gtts_class, misc_cog, mock_ctx): """Test TTS command with gTTS error.""" await misc_cog.tts.callback(misc_cog, mock_ctx, tts_text="Hello world") @@ -197,7 +197,7 @@ async def test_tts_command_error(self, mock_send_error, mock_gtts_class, misc_co mock_send_error.assert_called_once_with(mock_ctx, messages.INVALID_MESSAGE) @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_error_msg') + @patch("src.bot.cogs.misc.bot_utils.send_error_msg") async def test_tts_command_empty_text(self, mock_send_error, misc_cog, mock_ctx): """Test TTS command with empty processed text.""" await misc_cog.tts.callback(misc_cog, mock_ctx, tts_text="") @@ -206,7 +206,7 @@ async def test_tts_command_empty_text(self, mock_send_error, misc_cog, mock_ctx) # Test echo command @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_msg') + @patch("src.bot.cogs.misc.bot_utils.send_msg") async def test_echo_command(self, mock_send_msg, misc_cog, mock_ctx): """Test echo command.""" await misc_cog.echo.callback(misc_cog, mock_ctx, msg="Hello world!") @@ -216,7 +216,7 @@ async def test_echo_command(self, mock_send_msg, misc_cog, mock_ctx): # Test ping command @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_embed') + @patch("src.bot.cogs.misc.bot_utils.send_embed") async def test_ping_command_good_latency(self, mock_send_embed, misc_cog, mock_ctx): """Test ping command with good latency.""" mock_ctx.subcommand_passed = None @@ -232,7 +232,7 @@ async def test_ping_command_good_latency(self, mock_send_embed, misc_cog, mock_c assert embed.color == discord.Color.green() @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_embed') + @patch("src.bot.cogs.misc.bot_utils.send_embed") async def test_ping_command_bad_latency(self, mock_send_embed, misc_cog, mock_ctx): """Test ping command with bad latency.""" mock_ctx.subcommand_passed = None @@ -254,7 +254,7 @@ async def test_ping_command_with_subcommand(self, misc_cog, mock_ctx): # Test lmgtfy command @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_msg') + @patch("src.bot.cogs.misc.bot_utils.send_msg") async def test_lmgtfy_command(self, mock_send_msg, misc_cog, mock_ctx): """Test lmgtfy command.""" await misc_cog.lmgtfy.callback(misc_cog, mock_ctx, user_msg="how to code in python") @@ -268,8 +268,8 @@ async def test_lmgtfy_command(self, mock_send_msg, misc_cog, mock_ctx): # Test invites command @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_msg') - @patch('src.bot.cogs.misc.chat_formatting.inline') + @patch("src.bot.cogs.misc.bot_utils.send_msg") + @patch("src.bot.cogs.misc.chat_formatting.inline") async def test_invites_command_no_invites(self, mock_inline, mock_send_msg, misc_cog, mock_ctx): """Test invites command with no invites.""" mock_ctx.subcommand_passed = None @@ -282,7 +282,7 @@ async def test_invites_command_no_invites(self, mock_inline, mock_send_msg, misc mock_send_msg.assert_called_once_with(mock_ctx, "inline_text") @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_embed') + @patch("src.bot.cogs.misc.bot_utils.send_embed") async def test_invites_command_with_invites(self, mock_send_embed, misc_cog, mock_ctx): """Test invites command with various invite types.""" mock_ctx.subcommand_passed = None @@ -329,9 +329,9 @@ async def test_invites_command_with_subcommand(self, misc_cog, mock_ctx): # Test serverinfo command @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_embed') - @patch('src.bot.cogs.misc.bot_utils.get_current_date_time') - @patch('src.bot.cogs.misc.bot_utils.convert_datetime_to_str_long') + @patch("src.bot.cogs.misc.bot_utils.send_embed") + @patch("src.bot.cogs.misc.bot_utils.get_current_date_time") + @patch("src.bot.cogs.misc.bot_utils.convert_datetime_to_str_long") async def test_serverinfo_command( self, mock_convert_datetime, mock_get_current_time, mock_send_embed, misc_cog, mock_ctx ): @@ -383,9 +383,9 @@ async def test_serverinfo_command_with_subcommand(self, misc_cog, mock_ctx): # Test userinfo command @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_embed') - @patch('src.bot.cogs.misc.bot_utils.get_object_member_by_str') - @patch('src.bot.cogs.misc.bot_utils.get_current_date_time') + @patch("src.bot.cogs.misc.bot_utils.send_embed") + @patch("src.bot.cogs.misc.bot_utils.get_object_member_by_str") + @patch("src.bot.cogs.misc.bot_utils.get_current_date_time") async def test_userinfo_command_self( self, mock_get_current_time, mock_get_member, mock_send_embed, misc_cog, mock_ctx ): @@ -406,9 +406,9 @@ async def test_userinfo_command_self( assert embed.color == discord.Color.blue() @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_embed') - @patch('src.bot.cogs.misc.bot_utils.get_object_member_by_str') - @patch('src.bot.cogs.misc.bot_utils.get_current_date_time') + @patch("src.bot.cogs.misc.bot_utils.send_embed") + @patch("src.bot.cogs.misc.bot_utils.get_object_member_by_str") + @patch("src.bot.cogs.misc.bot_utils.get_current_date_time") async def test_userinfo_command_other_user( self, mock_get_current_time, mock_get_member, mock_send_embed, misc_cog, mock_ctx, mock_member ): @@ -429,8 +429,8 @@ async def test_userinfo_command_other_user( # Test about command @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_embed') - @patch('src.bot.cogs.misc.bot_utils.get_bot_stats') + @patch("src.bot.cogs.misc.bot_utils.send_embed") + @patch("src.bot.cogs.misc.bot_utils.get_bot_stats") async def test_about_command(self, mock_get_bot_stats, mock_send_embed, misc_cog, mock_ctx): """Test about command.""" mock_ctx.subcommand_passed = None @@ -621,7 +621,7 @@ def test_get_user_info(self, misc_cog, mock_ctx, mock_member): role3.name = "Role3" mock_member.roles = [role1, role2, role3] - with patch('src.bot.cogs.misc.bot_utils.get_current_date_time', return_value=datetime.now(UTC)): + with patch("src.bot.cogs.misc.bot_utils.get_current_date_time", return_value=datetime.now(UTC)): result = misc_cog._get_user_info(mock_ctx.guild, mock_member) assert "created_on" in result @@ -699,7 +699,7 @@ async def test_setup_function(self, mock_bot): def test_misc_cog_inheritance(self, misc_cog): """Test that Misc cog properly inherits from commands.Cog.""" assert isinstance(misc_cog, commands.Cog) - assert hasattr(misc_cog, 'bot') + assert hasattr(misc_cog, "bot") def test_process_tts_text_no_special_tokens(self, misc_cog, mock_ctx): """Test _process_tts_text with no special tokens.""" @@ -718,9 +718,9 @@ def test_process_tts_text_with_special_tokens(self, misc_cog, mock_ctx): # Test serverinfo with no icon (line 168) @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_embed') - @patch('src.bot.cogs.misc.bot_utils.get_current_date_time') - @patch('src.bot.cogs.misc.bot_utils.convert_datetime_to_str_long') + @patch("src.bot.cogs.misc.bot_utils.send_embed") + @patch("src.bot.cogs.misc.bot_utils.get_current_date_time") + @patch("src.bot.cogs.misc.bot_utils.convert_datetime_to_str_long") async def test_serverinfo_no_icon( self, mock_convert_datetime, mock_get_current_time, mock_send_embed, misc_cog, mock_ctx ): @@ -752,9 +752,9 @@ async def test_serverinfo_no_icon( # Test userinfo with no avatar (line 204) @pytest.mark.asyncio - @patch('src.bot.cogs.misc.bot_utils.send_embed') - @patch('src.bot.cogs.misc.bot_utils.get_object_member_by_str') - @patch('src.bot.cogs.misc.bot_utils.get_current_date_time') + @patch("src.bot.cogs.misc.bot_utils.send_embed") + @patch("src.bot.cogs.misc.bot_utils.get_object_member_by_str") + @patch("src.bot.cogs.misc.bot_utils.get_current_date_time") async def test_userinfo_no_avatar( self, mock_get_current_time, mock_get_member, mock_send_embed, misc_cog, mock_ctx ): diff --git a/tests/unit/bot/cogs/test_open_ai.py b/tests/unit/bot/cogs/test_open_ai.py index 39c83905..02ef5221 100644 --- a/tests/unit/bot/cogs/test_open_ai.py +++ b/tests/unit/bot/cogs/test_open_ai.py @@ -8,7 +8,7 @@ from discord.ext import commands from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.open_ai import OpenAi @@ -83,7 +83,7 @@ def test_init(self, mock_bot): def test_openai_client_property_creates_client(self, openai_cog): """Test that openai_client property creates client on first access.""" - with patch('src.bot.cogs.open_ai.OpenAI') as mock_openai_class: + with patch("src.bot.cogs.open_ai.OpenAI") as mock_openai_class: mock_client = MagicMock() mock_openai_class.return_value = mock_client @@ -103,15 +103,15 @@ def test_openai_client_property_returns_existing_client(self, openai_cog): assert client == mock_client @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') - @patch('src.bot.cogs.open_ai.bot_utils.send_embed') + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") async def test_ai_command_success( self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings, mock_openai_response ): """Test successful AI command execution.""" mock_get_settings.return_value = mock_bot_settings - with patch.object(openai_cog, '_get_ai_response', return_value="AI response here"): + with patch.object(openai_cog, "_get_ai_response", return_value="AI response here"): await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="What is Python?") mock_ctx.message.channel.typing.assert_called_once() @@ -125,13 +125,13 @@ async def test_ai_command_success( assert embed.author.icon_url == "https://example.com/avatar.png" @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') - @patch('src.bot.cogs.open_ai.bot_utils.send_embed') + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") async def test_ai_command_error(self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings): """Test AI command with OpenAI API error.""" mock_get_settings.return_value = mock_bot_settings - with patch.object(openai_cog, '_get_ai_response', side_effect=Exception("API Error")): + with patch.object(openai_cog, "_get_ai_response", side_effect=Exception("API Error")): await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="What is Python?") mock_ctx.message.channel.typing.assert_called_once() @@ -147,7 +147,7 @@ async def test_ai_command_error(self, mock_send_embed, mock_get_settings, openai openai_cog.bot.log.error.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') + @patch("src.bot.cogs.open_ai.get_bot_settings") async def test_get_ai_response_success( self, mock_get_settings, openai_cog, mock_bot_settings, mock_openai_response ): @@ -167,19 +167,19 @@ async def test_get_ai_response_success( mock_client.chat.completions.create.assert_called_once() call_args = mock_client.chat.completions.create.call_args - assert call_args[1]['model'] == "gpt-3.5-turbo" - assert call_args[1]['max_tokens'] == 1000 - assert call_args[1]['temperature'] == pytest.approx(0.7) + assert call_args[1]["model"] == "gpt-3.5-turbo" + assert call_args[1]["max_tokens"] == 1000 + assert call_args[1]["temperature"] == pytest.approx(0.7) # Verify message types and content - messages = call_args[1]['messages'] + messages = call_args[1]["messages"] assert len(messages) == 2 - assert messages[0]['role'] == "system" - assert messages[1]['role'] == "user" - assert messages[1]['content'] == "What is Python?" + assert messages[0]["role"] == "system" + assert messages[1]["role"] == "user" + assert messages[1]["content"] == "What is Python?" @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') + @patch("src.bot.cogs.open_ai.get_bot_settings") async def test_get_ai_response_with_leading_trailing_spaces( self, mock_get_settings, openai_cog, mock_bot_settings, mock_openai_response ): @@ -253,8 +253,8 @@ def test_create_ai_embed_no_bot_avatar(self, openai_cog, mock_ctx): assert "UTC" in embed.footer.text @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') - @patch('src.bot.cogs.open_ai.bot_utils.send_embed') + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") async def test_ai_command_with_different_models( self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_openai_response ): @@ -273,11 +273,11 @@ async def test_ai_command_with_different_models( # Verify correct model was used call_args = mock_client.chat.completions.create.call_args - assert call_args[1]['model'] == "gpt-4" + assert call_args[1]["model"] == "gpt-4" @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') - @patch('src.bot.cogs.open_ai.bot_utils.send_embed') + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") async def test_ai_command_with_long_question( self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings ): @@ -285,7 +285,7 @@ async def test_ai_command_with_long_question( mock_get_settings.return_value = mock_bot_settings long_question = "What is " + "very " * 1000 + "long question?" - with patch.object(openai_cog, '_get_ai_response', return_value="Short answer"): + with patch.object(openai_cog, "_get_ai_response", return_value="Short answer"): await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text=long_question) mock_send_embed.assert_called_once() @@ -293,8 +293,8 @@ async def test_ai_command_with_long_question( assert embed.description == "Short answer" @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') - @patch('src.bot.cogs.open_ai.bot_utils.send_embed') + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") async def test_ai_command_with_special_characters( self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings ): @@ -302,7 +302,7 @@ async def test_ai_command_with_special_characters( mock_get_settings.return_value = mock_bot_settings special_question = "What is 2+2? 🤔 And émojis & spéciál chars?" - with patch.object(openai_cog, '_get_ai_response', return_value="4! 😊"): + with patch.object(openai_cog, "_get_ai_response", return_value="4! 😊"): await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text=special_question) mock_send_embed.assert_called_once() @@ -310,7 +310,7 @@ async def test_ai_command_with_special_characters( assert embed.description == "4! 😊" @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') + @patch("src.bot.cogs.open_ai.get_bot_settings") async def test_get_ai_response_system_message_content( self, mock_get_settings, openai_cog, mock_bot_settings, mock_openai_response ): @@ -324,13 +324,13 @@ async def test_get_ai_response_system_message_content( await openai_cog._get_ai_response("Test message") - messages = mock_client.chat.completions.create.call_args[1]['messages'] + messages = mock_client.chat.completions.create.call_args[1]["messages"] system_message = messages[0] expected_content = "You are a helpful AI assistant. Provide clear, concise, and accurate responses." - assert system_message['content'] == expected_content + assert system_message["content"] == expected_content @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') + @patch("src.bot.cogs.open_ai.get_bot_settings") async def test_get_ai_response_api_parameters( self, mock_get_settings, openai_cog, mock_bot_settings, mock_openai_response ): @@ -345,11 +345,11 @@ async def test_get_ai_response_api_parameters( await openai_cog._get_ai_response("Test message") call_args = mock_client.chat.completions.create.call_args[1] - assert call_args['max_tokens'] == 1000 - assert call_args['temperature'] == pytest.approx(0.7) - assert call_args['model'] == "gpt-3.5-turbo" + assert call_args["max_tokens"] == 1000 + assert call_args["temperature"] == pytest.approx(0.7) + assert call_args["model"] == "gpt-3.5-turbo" - @patch('src.bot.cogs.open_ai.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.open_ai.bot_utils.get_current_date_time_str_long") def test_create_ai_embed_footer(self, mock_get_datetime, openai_cog, mock_ctx): """Test that embed footer contains correct timestamp.""" mock_get_datetime.return_value = "2023-01-01 12:00:00" @@ -374,11 +374,11 @@ async def test_setup_function(self, mock_bot): def test_openai_cog_inheritance(self, openai_cog): """Test that OpenAi cog properly inherits from commands.Cog.""" assert isinstance(openai_cog, commands.Cog) - assert hasattr(openai_cog, 'bot') + assert hasattr(openai_cog, "bot") @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') - @patch('src.bot.cogs.open_ai.bot_utils.send_embed') + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") async def test_ai_command_error_logging( self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings ): @@ -386,7 +386,7 @@ async def test_ai_command_error_logging( mock_get_settings.return_value = mock_bot_settings test_error = Exception("Test API Error") - with patch.object(openai_cog, '_get_ai_response', side_effect=test_error): + with patch.object(openai_cog, "_get_ai_response", side_effect=test_error): await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="Test question") # Verify error was logged with correct message @@ -396,15 +396,15 @@ async def test_ai_command_error_logging( assert "Test API Error" in log_call @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') - @patch('src.bot.cogs.open_ai.bot_utils.send_embed') + @patch("src.bot.cogs.open_ai.get_bot_settings") + @patch("src.bot.cogs.open_ai.bot_utils.send_embed") async def test_ai_command_send_embed_parameters( self, mock_send_embed, mock_get_settings, openai_cog, mock_ctx, mock_bot_settings ): """Test that send_embed is called with correct parameters.""" mock_get_settings.return_value = mock_bot_settings - with patch.object(openai_cog, '_get_ai_response', return_value="Test response"): + with patch.object(openai_cog, "_get_ai_response", return_value="Test response"): await openai_cog.ai.callback(openai_cog, mock_ctx, msg_text="Test question") # Verify send_embed was called with ctx, embed, and False @@ -415,7 +415,7 @@ async def test_ai_command_send_embed_parameters( assert call_args[2] is False # dm parameter @pytest.mark.asyncio - @patch('src.bot.cogs.open_ai.get_bot_settings') + @patch("src.bot.cogs.open_ai.get_bot_settings") async def test_get_ai_response_empty_response(self, mock_get_settings, openai_cog, mock_bot_settings): """Test _get_ai_response with empty response from OpenAI.""" mock_get_settings.return_value = mock_bot_settings diff --git a/tests/unit/bot/cogs/test_owner.py b/tests/unit/bot/cogs/test_owner.py index d141fc9a..ce10c4bf 100644 --- a/tests/unit/bot/cogs/test_owner.py +++ b/tests/unit/bot/cogs/test_owner.py @@ -8,7 +8,7 @@ from discord.ext import commands from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.owner import Owner from src.bot.constants import messages, variables @@ -88,7 +88,7 @@ def test_init(self, mock_bot): assert cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.owner.bot_utils.invoke_subcommand') + @patch("src.bot.cogs.owner.bot_utils.invoke_subcommand") async def test_owner_group_command(self, mock_invoke, owner_cog, mock_ctx): """Test owner group command.""" mock_invoke.return_value = "mock_command" @@ -100,8 +100,8 @@ async def test_owner_group_command(self, mock_invoke, owner_cog, mock_ctx): # Test prefix change command @pytest.mark.asyncio - @patch('src.bot.cogs.owner.BotConfigsDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.BotConfigsDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_change_prefix_success(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx): """Test successful prefix change.""" mock_dal = AsyncMock() @@ -131,8 +131,8 @@ async def test_owner_change_prefix_invalid(self, owner_cog, mock_ctx): assert ", ".join(variables.ALLOWED_PREFIXES) in error_msg @pytest.mark.asyncio - @patch('src.bot.cogs.owner.BotConfigsDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.BotConfigsDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_change_prefix_with_activity_update(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx): """Test prefix change with bot activity update.""" mock_dal = AsyncMock() @@ -142,13 +142,13 @@ async def test_owner_change_prefix_with_activity_update(self, mock_send_embed, m # Verify activity was updated owner_cog.bot.change_presence.assert_called_once() - activity_call = owner_cog.bot.change_presence.call_args[1]['activity'] + activity_call = owner_cog.bot.change_presence.call_args[1]["activity"] assert isinstance(activity_call, discord.Game) assert activity_call.name == "Test Game | %help" @pytest.mark.asyncio - @patch('src.bot.cogs.owner.BotConfigsDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.BotConfigsDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_change_prefix_no_activity(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx): """Test prefix change when bot has no activity.""" owner_cog.bot.user.activity = None @@ -167,8 +167,8 @@ async def test_owner_change_prefix_no_activity(self, mock_send_embed, mock_dal_c mock_send_embed.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.owner.BotConfigsDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.BotConfigsDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_change_prefix_non_playing_activity(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx): """Test prefix change when bot has non-playing activity.""" # Set the guild's bot member activity type to listening (not playing) @@ -184,9 +184,9 @@ async def test_owner_change_prefix_non_playing_activity(self, mock_send_embed, m # Test description update command @pytest.mark.asyncio - @patch('src.bot.cogs.owner.BotConfigsDal') - @patch('src.bot.cogs.owner.bot_utils.delete_message') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.BotConfigsDal") + @patch("src.bot.cogs.owner.bot_utils.delete_message") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_description_success( self, mock_send_embed, mock_delete_msg, mock_dal_class, owner_cog, mock_ctx ): @@ -211,8 +211,8 @@ async def test_owner_description_success( # Test servers list command @pytest.mark.asyncio - @patch('src.bot.cogs.owner.ServersDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.ServersDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_servers_success(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx, mock_server_data): """Test successful servers list.""" mock_dal = AsyncMock() @@ -234,8 +234,8 @@ async def test_owner_servers_success(self, mock_send_embed, mock_dal_class, owne assert mock_send_embed.call_args[0][2] is True @pytest.mark.asyncio - @patch('src.bot.cogs.owner.ServersDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.ServersDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_servers_no_servers(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx): """Test servers list with no servers.""" mock_dal = AsyncMock() @@ -254,8 +254,8 @@ async def test_owner_servers_no_servers(self, mock_send_embed, mock_dal_class, o assert result == mock_send_embed.return_value @pytest.mark.asyncio - @patch('src.bot.cogs.owner.ServersDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.ServersDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_servers_many_servers(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx): """Test servers list with more than 25 servers (pagination).""" # Create 30 mock servers @@ -285,8 +285,8 @@ async def test_owner_servers_many_servers(self, mock_send_embed, mock_dal_class, assert embed.footer.text == "Total servers: 30" @pytest.mark.asyncio - @patch('src.bot.cogs.owner.ServersDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.ServersDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_servers_no_bot_avatar( self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx, mock_server_data ): @@ -310,12 +310,12 @@ async def test_update_bot_activity_prefix_with_activity(self, owner_cog): await owner_cog._update_bot_activity_prefix("$") owner_cog.bot.change_presence.assert_called_once() - activity_call = owner_cog.bot.change_presence.call_args[1]['activity'] + activity_call = owner_cog.bot.change_presence.call_args[1]["activity"] assert isinstance(activity_call, discord.Game) assert activity_call.name == "Test Game | $help" # Check status and activity parameters - assert owner_cog.bot.change_presence.call_args[1]['status'] == discord.Status.online + assert owner_cog.bot.change_presence.call_args[1]["status"] == discord.Status.online @pytest.mark.asyncio async def test_update_bot_activity_prefix_no_activity(self, owner_cog): @@ -357,8 +357,8 @@ def test_create_owner_embed(self, owner_cog): assert embed.color == discord.Color.gold() @pytest.mark.asyncio - @patch('src.bot.cogs.owner.BotConfigsDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.BotConfigsDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_change_prefix_all_valid_prefixes(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx): """Test prefix change with all valid prefixes.""" mock_dal = AsyncMock() @@ -369,8 +369,8 @@ async def test_owner_change_prefix_all_valid_prefixes(self, mock_send_embed, moc assert owner_cog.bot.command_prefix == prefix @pytest.mark.asyncio - @patch('src.bot.cogs.owner.BotConfigsDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.BotConfigsDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_description_with_special_characters( self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx ): @@ -380,29 +380,29 @@ async def test_owner_description_with_special_characters( special_desc = "Bot with émojis 🤖 and spéciál chars & symbols!" - with patch('src.bot.cogs.owner.bot_utils.delete_message'): + with patch("src.bot.cogs.owner.bot_utils.delete_message"): await owner_cog.owner_description.callback(owner_cog, mock_ctx, desc=special_desc) assert owner_cog.bot.description == special_desc mock_dal.update_bot_description.assert_called_once_with(special_desc) @pytest.mark.asyncio - @patch('src.bot.cogs.owner.BotConfigsDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.BotConfigsDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_description_empty_string(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx): """Test description update with empty string.""" mock_dal = AsyncMock() mock_dal_class.return_value = mock_dal - with patch('src.bot.cogs.owner.bot_utils.delete_message'): + with patch("src.bot.cogs.owner.bot_utils.delete_message"): await owner_cog.owner_description.callback(owner_cog, mock_ctx, desc="") assert owner_cog.bot.description == "" mock_dal.update_bot_description.assert_called_once_with("") @pytest.mark.asyncio - @patch('src.bot.cogs.owner.ServersDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.ServersDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_servers_exactly_25_servers(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx): """Test servers list with exactly 25 servers (edge case).""" # Create exactly 25 mock servers @@ -438,7 +438,7 @@ async def test_update_bot_activity_prefix_complex_game_name(self, owner_cog): await owner_cog._update_bot_activity_prefix("$") # Should only take the first part before the first pipe - activity_call = owner_cog.bot.change_presence.call_args[1]['activity'] + activity_call = owner_cog.bot.change_presence.call_args[1]["activity"] assert activity_call.name == "Complex | $help" @pytest.mark.asyncio @@ -456,11 +456,11 @@ async def test_setup_function(self, mock_bot): def test_owner_cog_inheritance(self, owner_cog): """Test that Owner cog properly inherits from commands.Cog.""" assert isinstance(owner_cog, commands.Cog) - assert hasattr(owner_cog, 'bot') + assert hasattr(owner_cog, "bot") @pytest.mark.asyncio - @patch('src.bot.cogs.owner.ServersDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.ServersDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_servers_return_value_none_case( self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx, mock_server_data ): @@ -474,8 +474,8 @@ async def test_owner_servers_return_value_none_case( assert result is None @pytest.mark.asyncio - @patch('src.bot.cogs.owner.BotConfigsDal') - @patch('src.bot.cogs.owner.bot_utils.send_embed') + @patch("src.bot.cogs.owner.BotConfigsDal") + @patch("src.bot.cogs.owner.bot_utils.send_embed") async def test_owner_change_prefix_case_sensitivity(self, mock_send_embed, mock_dal_class, owner_cog, mock_ctx): """Test that prefix validation is case-sensitive.""" mock_dal = AsyncMock() @@ -498,5 +498,5 @@ async def test_update_bot_activity_prefix_with_no_pipe_in_activity(self, owner_c await owner_cog._update_bot_activity_prefix("$") - activity_call = owner_cog.bot.change_presence.call_args[1]['activity'] + activity_call = owner_cog.bot.change_presence.call_args[1]["activity"] assert activity_call.name == "SimpleGameName | $help" diff --git a/tests/unit/bot/constants/test_settings.py b/tests/unit/bot/constants/test_settings.py index 93083973..18dd15c6 100644 --- a/tests/unit/bot/constants/test_settings.py +++ b/tests/unit/bot/constants/test_settings.py @@ -14,14 +14,14 @@ def test_default_values_structure(self): settings = BotSettings() # Test that all expected fields exist - assert hasattr(settings, 'prefix') - assert hasattr(settings, 'token') - assert hasattr(settings, 'openai_model') - assert hasattr(settings, 'openai_api_key') - assert hasattr(settings, 'bg_activity_timer') - assert hasattr(settings, 'allowed_dm_commands') - assert hasattr(settings, 'embed_color') - assert hasattr(settings, 'admin_cooldown') + assert hasattr(settings, "prefix") + assert hasattr(settings, "token") + assert hasattr(settings, "openai_model") + assert hasattr(settings, "openai_api_key") + assert hasattr(settings, "bg_activity_timer") + assert hasattr(settings, "allowed_dm_commands") + assert hasattr(settings, "embed_color") + assert hasattr(settings, "admin_cooldown") # Test types are correct (allowing for None due to Optional) assert isinstance(settings.prefix, (str, type(None))) @@ -178,10 +178,10 @@ def test_settings_with_actual_env_file(self): settings = get_bot_settings() # Just verify the structure is correct - assert hasattr(settings, 'prefix') - assert hasattr(settings, 'token') - assert hasattr(settings, 'admin_cooldown') - assert hasattr(settings, 'embed_color') + assert hasattr(settings, "prefix") + assert hasattr(settings, "token") + assert hasattr(settings, "admin_cooldown") + assert hasattr(settings, "embed_color") # Verify types assert isinstance(settings.prefix, (str, type(None))) diff --git a/tests/unit/bot/events/test_bot_events.py b/tests/unit/bot/events/test_bot_events.py index cbe7eaf1..ddf1efbc 100644 --- a/tests/unit/bot/events/test_bot_events.py +++ b/tests/unit/bot/events/test_bot_events.py @@ -7,7 +7,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.events.on_command import CommandLogger, OnCommand from src.bot.cogs.events.on_disconnect import OnDisconnect @@ -16,6 +16,7 @@ from src.bot.cogs.events.on_guild_channel_update import OnGuildChannelUpdate from src.bot.cogs.events.on_guild_remove import GuildCleanupHandler, OnGuildRemove from src.bot.cogs.events.on_presence_update import OnPresenceUpdate +from src.bot.constants import messages @pytest.fixture @@ -30,7 +31,6 @@ def mock_bot(): bot.user = MagicMock() bot.user.name = "TestBot" bot.user.__str__ = MagicMock(return_value="TestBot#1234") - bot.event = MagicMock(side_effect=lambda func: func) bot.add_cog = AsyncMock(return_value=None) return bot @@ -190,10 +190,8 @@ async def test_on_command_event_calls_logger(self, mock_bot, mock_ctx): """Test on_command event handler calls the command logger.""" cog = OnCommand(mock_bot) - # Access the event handler registered via bot.event - on_command_event = mock_bot.event.call_args_list[0][0][0] - - await on_command_event(mock_ctx) + # Call the listener method directly + await cog.on_command(mock_ctx) # Verify the log was called (command_logger.log_command_execution was invoked) mock_bot.log.info.assert_called_once() @@ -206,10 +204,8 @@ async def test_on_command_event_exception(self, mock_bot, mock_ctx): # Patch command_logger to raise an exception cog.command_logger.log_command_execution = MagicMock(side_effect=RuntimeError("Logger error")) - # Access the event handler registered via bot.event - on_command_event = mock_bot.event.call_args_list[0][0][0] - - await on_command_event(mock_ctx) + # Call the listener method directly + await cog.on_command(mock_ctx) mock_bot.log.error.assert_called_once() error_call = mock_bot.log.error.call_args[0][0] @@ -221,7 +217,7 @@ def test_on_command_cog_inheritance(self, mock_bot): cog = OnCommand(mock_bot) assert isinstance(cog, commands.Cog) - assert hasattr(cog, 'bot') + assert hasattr(cog, "bot") # ============================================================================= @@ -252,28 +248,24 @@ async def test_setup_function(self, mock_bot): @pytest.mark.asyncio async def test_on_disconnect_event_logs_warning(self, mock_bot): """Test on_disconnect event logs a warning message.""" - OnDisconnect(mock_bot) - - # Access the event handler registered via bot.event - on_disconnect_event = mock_bot.event.call_args_list[0][0][0] + cog = OnDisconnect(mock_bot) - await on_disconnect_event() + # Call the listener method directly + await cog.on_disconnect() - # Since BOT_DISCONNECTED does not exist in messages, the else branch is used - mock_bot.log.warning.assert_called_once_with(f"Bot {mock_bot.user} disconnected from Discord") + mock_bot.log.warning.assert_called_once_with(messages.bot_disconnected(mock_bot.user)) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_disconnect.messages') + @patch("src.bot.cogs.events.on_disconnect.messages") async def test_on_disconnect_event_with_bot_disconnected_message(self, mock_messages, mock_bot): - """Test on_disconnect event with BOT_DISCONNECTED message defined.""" - mock_messages.BOT_DISCONNECTED = "Bot {} has disconnected!" - - OnDisconnect(mock_bot) + """Test on_disconnect event calls bot_disconnected function.""" + mock_messages.bot_disconnected.return_value = f"Bot {mock_bot.user} has disconnected!" - on_disconnect_event = mock_bot.event.call_args_list[0][0][0] + cog = OnDisconnect(mock_bot) - await on_disconnect_event() + await cog.on_disconnect() + mock_messages.bot_disconnected.assert_called_once_with(mock_bot.user) mock_bot.log.warning.assert_called_once_with(f"Bot {mock_bot.user} has disconnected!") @pytest.mark.asyncio @@ -281,13 +273,11 @@ async def test_on_disconnect_event_exception(self, mock_bot): """Test on_disconnect event when exception occurs during logging.""" mock_bot.log.warning.side_effect = RuntimeError("Logging failure") - OnDisconnect(mock_bot) - - on_disconnect_event = mock_bot.event.call_args_list[0][0][0] + cog = OnDisconnect(mock_bot) # Should not raise; falls through to print fallback - with patch('builtins.print') as mock_print: - await on_disconnect_event() + with patch("builtins.print") as mock_print: + await cog.on_disconnect() mock_print.assert_called_once() print_call = mock_print.call_args[0][0] assert "Bot disconnected - logging failed" in print_call @@ -298,7 +288,7 @@ def test_on_disconnect_cog_inheritance(self, mock_bot): cog = OnDisconnect(mock_bot) assert isinstance(cog, commands.Cog) - assert hasattr(cog, 'bot') + assert hasattr(cog, "bot") # ============================================================================= @@ -329,15 +319,13 @@ async def test_setup_function(self, mock_bot): @pytest.mark.asyncio async def test_on_guild_channel_create_event(self, mock_bot): """Test on_guild_channel_create event handler (currently a no-op).""" - OnGuildChannelCreate(mock_bot) - - on_guild_channel_create_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildChannelCreate(mock_bot) mock_channel = MagicMock() mock_channel.name = "new-channel" # Should not raise any exception - await on_guild_channel_create_event(mock_channel) + await cog.on_guild_channel_create(mock_channel) def test_on_guild_channel_create_cog_inheritance(self, mock_bot): """Test that OnGuildChannelCreate cog properly inherits from commands.Cog.""" @@ -345,7 +333,7 @@ def test_on_guild_channel_create_cog_inheritance(self, mock_bot): cog = OnGuildChannelCreate(mock_bot) assert isinstance(cog, commands.Cog) - assert hasattr(cog, 'bot') + assert hasattr(cog, "bot") # ============================================================================= @@ -376,15 +364,13 @@ async def test_setup_function(self, mock_bot): @pytest.mark.asyncio async def test_on_guild_channel_delete_event(self, mock_bot): """Test on_guild_channel_delete event handler (currently a no-op).""" - OnGuildChannelDelete(mock_bot) - - on_guild_channel_delete_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildChannelDelete(mock_bot) mock_channel = MagicMock() mock_channel.name = "deleted-channel" # Should not raise any exception - await on_guild_channel_delete_event(mock_channel) + await cog.on_guild_channel_delete(mock_channel) def test_on_guild_channel_delete_cog_inheritance(self, mock_bot): """Test that OnGuildChannelDelete cog properly inherits from commands.Cog.""" @@ -392,7 +378,7 @@ def test_on_guild_channel_delete_cog_inheritance(self, mock_bot): cog = OnGuildChannelDelete(mock_bot) assert isinstance(cog, commands.Cog) - assert hasattr(cog, 'bot') + assert hasattr(cog, "bot") # ============================================================================= @@ -423,9 +409,7 @@ async def test_setup_function(self, mock_bot): @pytest.mark.asyncio async def test_on_guild_channel_update_event(self, mock_bot): """Test on_guild_channel_update event handler (currently a no-op).""" - OnGuildChannelUpdate(mock_bot) - - on_guild_channel_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildChannelUpdate(mock_bot) mock_before = MagicMock() mock_before.name = "old-channel" @@ -433,7 +417,7 @@ async def test_on_guild_channel_update_event(self, mock_bot): mock_after.name = "new-channel" # Should not raise any exception - await on_guild_channel_update_event(mock_before, mock_after) + await cog.on_guild_channel_update(mock_before, mock_after) def test_on_guild_channel_update_cog_inheritance(self, mock_bot): """Test that OnGuildChannelUpdate cog properly inherits from commands.Cog.""" @@ -441,7 +425,7 @@ def test_on_guild_channel_update_cog_inheritance(self, mock_bot): cog = OnGuildChannelUpdate(mock_bot) assert isinstance(cog, commands.Cog) - assert hasattr(cog, 'bot') + assert hasattr(cog, "bot") # ============================================================================= @@ -458,7 +442,7 @@ def test_init(self, mock_bot): assert handler.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_remove.ServersDal') + @patch("src.bot.cogs.events.on_guild_remove.ServersDal") async def test_cleanup_server_data_success(self, mock_dal_class, mock_bot, mock_guild): """Test successful cleanup of server data.""" mock_dal = AsyncMock() @@ -476,7 +460,7 @@ async def test_cleanup_server_data_success(self, mock_dal_class, mock_bot, mock_ ) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_remove.ServersDal') + @patch("src.bot.cogs.events.on_guild_remove.ServersDal") async def test_cleanup_server_data_failure(self, mock_dal_class, mock_bot, mock_guild): """Test cleanup of server data when exception occurs.""" mock_dal = AsyncMock() @@ -495,7 +479,7 @@ async def test_cleanup_server_data_failure(self, mock_dal_class, mock_bot, mock_ assert str(mock_guild.id) in error_call @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_remove.ServersDal') + @patch("src.bot.cogs.events.on_guild_remove.ServersDal") async def test_cleanup_server_data_dal_init_failure(self, mock_dal_class, mock_bot, mock_guild): """Test cleanup of server data when DAL initialization fails.""" mock_dal_class.side_effect = RuntimeError("DAL init error") @@ -536,7 +520,7 @@ async def test_setup_function(self, mock_bot): assert added_cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_remove.ServersDal') + @patch("src.bot.cogs.events.on_guild_remove.ServersDal") async def test_on_guild_remove_event_success(self, mock_dal_class, mock_bot, mock_guild): """Test on_guild_remove event handler with successful cleanup.""" mock_dal = AsyncMock() @@ -544,9 +528,8 @@ async def test_on_guild_remove_event_success(self, mock_dal_class, mock_bot, moc cog = OnGuildRemove(mock_bot) - on_guild_remove_event = mock_bot.event.call_args_list[0][0][0] - - await on_guild_remove_event(mock_guild) + # Call the listener method directly + await cog.on_guild_remove(mock_guild) # Verify info log about removal mock_bot.log.info.assert_any_call(f"Bot removed from guild: {mock_guild.name} (ID: {mock_guild.id})") @@ -554,7 +537,7 @@ async def test_on_guild_remove_event_success(self, mock_dal_class, mock_bot, moc mock_dal.delete_server.assert_called_once_with(mock_guild.id) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_remove.ServersDal') + @patch("src.bot.cogs.events.on_guild_remove.ServersDal") async def test_on_guild_remove_event_cleanup_failure(self, mock_dal_class, mock_bot, mock_guild): """Test on_guild_remove event handler when cleanup fails.""" mock_dal = AsyncMock() @@ -563,9 +546,8 @@ async def test_on_guild_remove_event_cleanup_failure(self, mock_dal_class, mock_ cog = OnGuildRemove(mock_bot) - on_guild_remove_event = mock_bot.event.call_args_list[0][0][0] - - await on_guild_remove_event(mock_guild) + # Call the listener method directly + await cog.on_guild_remove(mock_guild) # Verify warning about incomplete cleanup mock_bot.log.warning.assert_called_once_with(f"Database cleanup may be incomplete for guild: {mock_guild.name}") @@ -578,9 +560,8 @@ async def test_on_guild_remove_event_general_exception(self, mock_bot, mock_guil # Make the info log raise to trigger the outer exception handler mock_bot.log.info.side_effect = RuntimeError("Unexpected error") - on_guild_remove_event = mock_bot.event.call_args_list[0][0][0] - - await on_guild_remove_event(mock_guild) + # Call the listener method directly + await cog.on_guild_remove(mock_guild) mock_bot.log.error.assert_called_once() error_call = mock_bot.log.error.call_args[0][0] @@ -592,7 +573,7 @@ def test_on_guild_remove_cog_inheritance(self, mock_bot): cog = OnGuildRemove(mock_bot) assert isinstance(cog, commands.Cog) - assert hasattr(cog, 'bot') + assert hasattr(cog, "bot") # ============================================================================= @@ -621,34 +602,32 @@ async def test_setup_function(self, mock_bot): assert added_cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_presence_update.gw2_utils.check_gw2_game_activity') + @patch("src.bot.cogs.events.on_presence_update.gw2_utils.check_gw2_game_activity") async def test_on_presence_update_non_bot_user(self, mock_check_gw2, mock_bot): """Test on_presence_update event for a non-bot user.""" - OnPresenceUpdate(mock_bot) - - on_presence_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnPresenceUpdate(mock_bot) mock_before = MagicMock() mock_after = MagicMock() mock_after.bot = False - await on_presence_update_event(mock_before, mock_after) + # Call the listener method directly + await cog.on_presence_update(mock_before, mock_after) mock_check_gw2.assert_called_once_with(mock_bot, mock_before, mock_after) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_presence_update.gw2_utils.check_gw2_game_activity') + @patch("src.bot.cogs.events.on_presence_update.gw2_utils.check_gw2_game_activity") async def test_on_presence_update_bot_user(self, mock_check_gw2, mock_bot): """Test on_presence_update event for a bot user (should be ignored).""" - OnPresenceUpdate(mock_bot) - - on_presence_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnPresenceUpdate(mock_bot) mock_before = MagicMock() mock_after = MagicMock() mock_after.bot = True - await on_presence_update_event(mock_before, mock_after) + # Call the listener method directly + await cog.on_presence_update(mock_before, mock_after) mock_check_gw2.assert_not_called() @@ -658,4 +637,4 @@ def test_on_presence_update_cog_inheritance(self, mock_bot): cog = OnPresenceUpdate(mock_bot) assert isinstance(cog, commands.Cog) - assert hasattr(cog, 'bot') + assert hasattr(cog, "bot") diff --git a/tests/unit/bot/events/test_member_events.py b/tests/unit/bot/events/test_member_events.py index 44cec306..091b06cc 100644 --- a/tests/unit/bot/events/test_member_events.py +++ b/tests/unit/bot/events/test_member_events.py @@ -6,7 +6,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.events.on_member_join import ( MemberJoinHandler, @@ -35,7 +35,6 @@ def mock_bot(): bot.user.id = 99999 bot.user.avatar = MagicMock() bot.user.avatar.url = "https://example.com/bot.png" - bot.event = MagicMock(side_effect=lambda func: func) bot.add_cog = AsyncMock() return bot @@ -70,7 +69,7 @@ def test_init(self, mock_bot): builder = WelcomeMessageBuilder(mock_bot) assert builder.bot == mock_bot - @patch('src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long") def test_create_join_embed_with_avatar(self, mock_datetime, mock_bot, mock_member): """Test create_join_embed when member has an avatar.""" mock_datetime.return_value = "2023-06-15 10:30:00" @@ -86,7 +85,7 @@ def test_create_join_embed_with_avatar(self, mock_datetime, mock_bot, mock_membe assert result.footer.text == "2023-06-15 10:30:00 UTC" assert result.footer.icon_url == mock_bot.user.avatar.url - @patch('src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long") def test_create_join_embed_without_avatar(self, mock_datetime, mock_bot, mock_member): """Test create_join_embed when member has no avatar.""" mock_datetime.return_value = "2023-06-15 10:30:00" @@ -101,7 +100,7 @@ def test_create_join_embed_without_avatar(self, mock_datetime, mock_bot, mock_me # Thumbnail should not be set when avatar is None assert result.thumbnail.url is None - @patch('src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long") def test_create_join_embed_with_exception(self, mock_datetime, mock_bot, mock_member): """Test create_join_embed when an exception occurs.""" mock_datetime.side_effect = Exception("DateTime error") @@ -115,7 +114,7 @@ def test_create_join_embed_with_exception(self, mock_datetime, mock_bot, mock_me assert "joined the server!" in result.description mock_bot.log.error.assert_called_once() - @patch('src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long") def test_create_join_message_success(self, mock_datetime, mock_bot, mock_member): """Test create_join_message with successful execution.""" mock_datetime.return_value = "2023-06-15 10:30:00" @@ -126,7 +125,7 @@ def test_create_join_message_success(self, mock_datetime, mock_bot, mock_member) expected = f"TestUser {messages.JOINED_THE_SERVER}\n2023-06-15 10:30:00" assert result == expected - @patch('src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long") def test_create_join_message_exception(self, mock_datetime, mock_bot, mock_member): """Test create_join_message when an exception occurs.""" mock_datetime.side_effect = Exception("DateTime error") @@ -153,7 +152,7 @@ def test_init(self, mock_bot): assert isinstance(handler.message_builder, WelcomeMessageBuilder) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_join.ServersDal') + @patch("src.bot.cogs.events.on_member_join.ServersDal") async def test_process_member_join_no_config(self, mock_dal_class, mock_bot, mock_member): """Test process_member_join when no server config is found.""" mock_dal = AsyncMock() @@ -168,7 +167,7 @@ async def test_process_member_join_no_config(self, mock_dal_class, mock_bot, moc assert "No server config found" in mock_bot.log.warning.call_args[0][0] @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_join.ServersDal') + @patch("src.bot.cogs.events.on_member_join.ServersDal") async def test_process_member_join_msg_on_join_false(self, mock_dal_class, mock_bot, mock_member): """Test process_member_join when msg_on_join is False.""" mock_dal = AsyncMock() @@ -183,9 +182,9 @@ async def test_process_member_join_msg_on_join_false(self, mock_dal_class, mock_ mock_bot.log.info.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_join.bot_utils.send_msg_to_system_channel', new_callable=AsyncMock) - @patch('src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_member_join.ServersDal') + @patch("src.bot.cogs.events.on_member_join.bot_utils.send_msg_to_system_channel", new_callable=AsyncMock) + @patch("src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_member_join.ServersDal") async def test_process_member_join_msg_on_join_true( self, mock_dal_class, mock_datetime, mock_send_msg, mock_bot, mock_member ): @@ -204,7 +203,7 @@ async def test_process_member_join_msg_on_join_true( assert "Welcome message sent" in mock_bot.log.info.call_args[0][0] @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_join.ServersDal') + @patch("src.bot.cogs.events.on_member_join.ServersDal") async def test_process_member_join_exception(self, mock_dal_class, mock_bot, mock_member): """Test process_member_join when an exception occurs.""" mock_dal = AsyncMock() @@ -218,8 +217,8 @@ async def test_process_member_join_exception(self, mock_dal_class, mock_bot, moc assert "Error processing member join" in mock_bot.log.error.call_args[0][0] @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_join.bot_utils.send_msg_to_system_channel', new_callable=AsyncMock) - @patch('src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_join.bot_utils.send_msg_to_system_channel", new_callable=AsyncMock) + @patch("src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long") async def test_send_welcome_message_success(self, mock_datetime, mock_send_msg, mock_bot, mock_member): """Test _send_welcome_message with successful execution.""" mock_datetime.return_value = "2023-06-15 10:30:00" @@ -237,8 +236,8 @@ async def test_send_welcome_message_success(self, mock_datetime, mock_send_msg, assert "Welcome message sent" in mock_bot.log.info.call_args[0][0] @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_join.bot_utils.send_msg_to_system_channel', new_callable=AsyncMock) - @patch('src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_join.bot_utils.send_msg_to_system_channel", new_callable=AsyncMock) + @patch("src.bot.cogs.events.on_member_join.bot_utils.get_current_date_time_str_long") async def test_send_welcome_message_exception(self, mock_datetime, mock_send_msg, mock_bot, mock_member): """Test _send_welcome_message when an exception occurs.""" mock_datetime.return_value = "2023-06-15 10:30:00" @@ -264,8 +263,6 @@ def test_init(self, mock_bot): cog = OnMemberJoin(mock_bot) assert cog.bot == mock_bot assert isinstance(cog.join_handler, MemberJoinHandler) - # Verify event was registered - mock_bot.event.assert_called_once() @pytest.mark.asyncio async def test_setup_function(self, mock_bot): @@ -293,7 +290,7 @@ def test_init(self, mock_bot): builder = FarewellMessageBuilder(mock_bot) assert builder.bot == mock_bot - @patch('src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long") def test_create_leave_embed_with_avatar(self, mock_datetime, mock_bot, mock_member): """Test create_leave_embed when member has an avatar.""" mock_datetime.return_value = "2023-06-15 10:30:00" @@ -309,7 +306,7 @@ def test_create_leave_embed_with_avatar(self, mock_datetime, mock_bot, mock_memb assert result.footer.text == "2023-06-15 10:30:00 UTC" assert result.footer.icon_url == mock_bot.user.avatar.url - @patch('src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long") def test_create_leave_embed_without_avatar(self, mock_datetime, mock_bot, mock_member): """Test create_leave_embed when member has no avatar.""" mock_datetime.return_value = "2023-06-15 10:30:00" @@ -324,7 +321,7 @@ def test_create_leave_embed_without_avatar(self, mock_datetime, mock_bot, mock_m # Thumbnail should not be set when avatar is None assert result.thumbnail.url is None - @patch('src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long") def test_create_leave_embed_with_exception(self, mock_datetime, mock_bot, mock_member): """Test create_leave_embed when an exception occurs.""" mock_datetime.side_effect = Exception("DateTime error") @@ -338,7 +335,7 @@ def test_create_leave_embed_with_exception(self, mock_datetime, mock_bot, mock_m assert "left the server!" in result.description mock_bot.log.error.assert_called_once() - @patch('src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long") def test_create_leave_message_success(self, mock_datetime, mock_bot, mock_member): """Test create_leave_message with successful execution.""" mock_datetime.return_value = "2023-06-15 10:30:00" @@ -349,7 +346,7 @@ def test_create_leave_message_success(self, mock_datetime, mock_bot, mock_member expected = f"TestUser {messages.LEFT_THE_SERVER}\n2023-06-15 10:30:00" assert result == expected - @patch('src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long") def test_create_leave_message_exception(self, mock_datetime, mock_bot, mock_member): """Test create_leave_message when an exception occurs.""" mock_datetime.side_effect = Exception("DateTime error") @@ -376,7 +373,7 @@ def test_init(self, mock_bot): assert isinstance(handler.message_builder, FarewellMessageBuilder) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_remove.ServersDal') + @patch("src.bot.cogs.events.on_member_remove.ServersDal") async def test_process_member_leave_bot_is_member(self, mock_dal_class, mock_bot, mock_member): """Test process_member_leave when the bot itself is the member leaving.""" mock_member.id = mock_bot.user.id # Same ID as bot @@ -388,7 +385,7 @@ async def test_process_member_leave_bot_is_member(self, mock_dal_class, mock_bot mock_dal_class.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_remove.ServersDal') + @patch("src.bot.cogs.events.on_member_remove.ServersDal") async def test_process_member_leave_no_config(self, mock_dal_class, mock_bot, mock_member): """Test process_member_leave when no server config is found.""" mock_dal = AsyncMock() @@ -403,7 +400,7 @@ async def test_process_member_leave_no_config(self, mock_dal_class, mock_bot, mo assert "No server config found" in mock_bot.log.warning.call_args[0][0] @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_remove.ServersDal') + @patch("src.bot.cogs.events.on_member_remove.ServersDal") async def test_process_member_leave_msg_on_leave_false(self, mock_dal_class, mock_bot, mock_member): """Test process_member_leave when msg_on_leave is False.""" mock_dal = AsyncMock() @@ -418,9 +415,9 @@ async def test_process_member_leave_msg_on_leave_false(self, mock_dal_class, moc mock_bot.log.info.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_remove.bot_utils.send_msg_to_system_channel', new_callable=AsyncMock) - @patch('src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_member_remove.ServersDal') + @patch("src.bot.cogs.events.on_member_remove.bot_utils.send_msg_to_system_channel", new_callable=AsyncMock) + @patch("src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_member_remove.ServersDal") async def test_process_member_leave_msg_on_leave_true( self, mock_dal_class, mock_datetime, mock_send_msg, mock_bot, mock_member ): @@ -439,7 +436,7 @@ async def test_process_member_leave_msg_on_leave_true( assert "Farewell message sent" in mock_bot.log.info.call_args[0][0] @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_remove.ServersDal') + @patch("src.bot.cogs.events.on_member_remove.ServersDal") async def test_process_member_leave_exception(self, mock_dal_class, mock_bot, mock_member): """Test process_member_leave when an exception occurs.""" mock_dal = AsyncMock() @@ -453,8 +450,8 @@ async def test_process_member_leave_exception(self, mock_dal_class, mock_bot, mo assert "Error processing member leave" in mock_bot.log.error.call_args[0][0] @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_remove.bot_utils.send_msg_to_system_channel', new_callable=AsyncMock) - @patch('src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_remove.bot_utils.send_msg_to_system_channel", new_callable=AsyncMock) + @patch("src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long") async def test_send_farewell_message_success(self, mock_datetime, mock_send_msg, mock_bot, mock_member): """Test _send_farewell_message with successful execution.""" mock_datetime.return_value = "2023-06-15 10:30:00" @@ -472,8 +469,8 @@ async def test_send_farewell_message_success(self, mock_datetime, mock_send_msg, assert "Farewell message sent" in mock_bot.log.info.call_args[0][0] @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_remove.bot_utils.send_msg_to_system_channel', new_callable=AsyncMock) - @patch('src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_remove.bot_utils.send_msg_to_system_channel", new_callable=AsyncMock) + @patch("src.bot.cogs.events.on_member_remove.bot_utils.get_current_date_time_str_long") async def test_send_farewell_message_exception(self, mock_datetime, mock_send_msg, mock_bot, mock_member): """Test _send_farewell_message when an exception occurs.""" mock_datetime.return_value = "2023-06-15 10:30:00" @@ -499,8 +496,6 @@ def test_init(self, mock_bot): cog = OnMemberRemove(mock_bot) assert cog.bot == mock_bot assert isinstance(cog.leave_handler, MemberLeaveHandler) - # Verify event was registered - mock_bot.event.assert_called_once() @pytest.mark.asyncio async def test_setup_function(self, mock_bot): @@ -527,8 +522,6 @@ def test_init(self, mock_bot): """Test OnUserUpdate cog initialization.""" cog = OnUserUpdate(mock_bot) assert cog.bot == mock_bot - # Verify event was registered - mock_bot.event.assert_called_once() @pytest.mark.asyncio async def test_setup_function(self, mock_bot): @@ -543,26 +536,26 @@ async def test_setup_function(self, mock_bot): assert added_cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_user_update.bot_utils.get_embed') + @patch("src.bot.cogs.events.on_user_update.bot_utils.get_embed") async def test_on_user_update_bot_user(self, mock_get_embed, mock_bot): """Test on_user_update with a bot user (should return early).""" before = MagicMock() after = MagicMock() after.bot = True - OnUserUpdate(mock_bot) - on_user_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnUserUpdate(mock_bot) - await on_user_update_event(before, after) + # Call the listener method directly + await cog.on_user_update(before, after) # Should not process bot users mock_get_embed.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_user_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_user_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_user_update.ServersDal') - @patch('src.bot.cogs.events.on_user_update.bot_utils.send_msg_to_system_channel', new_callable=AsyncMock) + @patch("src.bot.cogs.events.on_user_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_user_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_user_update.ServersDal") + @patch("src.bot.cogs.events.on_user_update.bot_utils.send_msg_to_system_channel", new_callable=AsyncMock) async def test_on_user_update_avatar_change( self, mock_send_msg, mock_dal_class, mock_datetime, mock_get_embed, mock_bot ): @@ -595,10 +588,10 @@ async def test_on_user_update_avatar_change( mock_dal_class.return_value = mock_dal mock_dal.get_server.return_value = {"msg_on_member_update": True} - OnUserUpdate(mock_bot) - on_user_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnUserUpdate(mock_bot) - await on_user_update_event(before, after) + # Call the listener method directly + await cog.on_user_update(before, after) # Verify embed has avatar field field_names = [f.name for f in embed.fields] @@ -609,10 +602,10 @@ async def test_on_user_update_avatar_change( mock_send_msg.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_user_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_user_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_user_update.ServersDal') - @patch('src.bot.cogs.events.on_user_update.bot_utils.send_msg_to_system_channel', new_callable=AsyncMock) + @patch("src.bot.cogs.events.on_user_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_user_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_user_update.ServersDal") + @patch("src.bot.cogs.events.on_user_update.bot_utils.send_msg_to_system_channel", new_callable=AsyncMock) async def test_on_user_update_name_change( self, mock_send_msg, mock_dal_class, mock_datetime, mock_get_embed, mock_bot ): @@ -645,10 +638,10 @@ async def test_on_user_update_name_change( mock_dal_class.return_value = mock_dal mock_dal.get_server.return_value = {"msg_on_member_update": True} - OnUserUpdate(mock_bot) - on_user_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnUserUpdate(mock_bot) - await on_user_update_event(before, after) + # Call the listener method directly + await cog.on_user_update(before, after) # Verify embed has name fields field_names = [f.name for f in embed.fields] @@ -659,10 +652,10 @@ async def test_on_user_update_name_change( mock_send_msg.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_user_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_user_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_user_update.ServersDal') - @patch('src.bot.cogs.events.on_user_update.bot_utils.send_msg_to_system_channel', new_callable=AsyncMock) + @patch("src.bot.cogs.events.on_user_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_user_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_user_update.ServersDal") + @patch("src.bot.cogs.events.on_user_update.bot_utils.send_msg_to_system_channel", new_callable=AsyncMock) async def test_on_user_update_discriminator_change( self, mock_send_msg, mock_dal_class, mock_datetime, mock_get_embed, mock_bot ): @@ -695,10 +688,10 @@ async def test_on_user_update_discriminator_change( mock_dal_class.return_value = mock_dal mock_dal.get_server.return_value = {"msg_on_member_update": True} - OnUserUpdate(mock_bot) - on_user_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnUserUpdate(mock_bot) - await on_user_update_event(before, after) + # Call the listener method directly + await cog.on_user_update(before, after) # Verify embed has discriminator fields field_names = [f.name for f in embed.fields] @@ -709,10 +702,10 @@ async def test_on_user_update_discriminator_change( mock_send_msg.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_user_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_user_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_user_update.ServersDal') - @patch('src.bot.cogs.events.on_user_update.bot_utils.send_msg_to_system_channel', new_callable=AsyncMock) + @patch("src.bot.cogs.events.on_user_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_user_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_user_update.ServersDal") + @patch("src.bot.cogs.events.on_user_update.bot_utils.send_msg_to_system_channel", new_callable=AsyncMock) async def test_on_user_update_no_changes( self, mock_send_msg, mock_dal_class, mock_datetime, mock_get_embed, mock_bot ): @@ -736,10 +729,10 @@ async def test_on_user_update_no_changes( after.name = "TestUser" after.discriminator = "1234" - OnUserUpdate(mock_bot) - on_user_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnUserUpdate(mock_bot) - await on_user_update_event(before, after) + # Call the listener method directly + await cog.on_user_update(before, after) # Verify no fields were added assert len(embed.fields) == 0 @@ -755,7 +748,7 @@ async def test_on_user_update_no_changes( class TestOnMemberJoinEvent: - """Test cases for the inner on_member_join event function registered in OnMemberJoin.__init__.""" + """Test cases for the on_member_join listener method in OnMemberJoin.""" @pytest.fixture def mock_bot(self): @@ -770,7 +763,6 @@ def mock_bot(self): bot.user.id = 99999 bot.user.avatar = MagicMock() bot.user.avatar.url = "https://example.com/bot.png" - bot.event = MagicMock(side_effect=lambda func: func) bot.add_cog = AsyncMock() return bot @@ -794,11 +786,10 @@ def mock_member(self): async def test_on_member_join_event_calls_handler(self, mock_bot, mock_member): """Test that the on_member_join event calls the join handler.""" cog = OnMemberJoin(mock_bot) - # Get the registered event function - on_member_join_event = mock_bot.event.call_args_list[0][0][0] - with patch.object(cog.join_handler, 'process_member_join', new_callable=AsyncMock) as mock_process: - await on_member_join_event(mock_member) + with patch.object(cog.join_handler, "process_member_join", new_callable=AsyncMock) as mock_process: + # Call the listener method directly + await cog.on_member_join(mock_member) mock_process.assert_called_once_with(mock_member) mock_bot.log.info.assert_called_once() assert "Member joined" in mock_bot.log.info.call_args[0][0] @@ -807,12 +798,12 @@ async def test_on_member_join_event_calls_handler(self, mock_bot, mock_member): async def test_on_member_join_event_handles_exception(self, mock_bot, mock_member): """Test that on_member_join catches exceptions and logs critical error.""" cog = OnMemberJoin(mock_bot) - on_member_join_event = mock_bot.event.call_args_list[0][0][0] with patch.object( - cog.join_handler, 'process_member_join', new_callable=AsyncMock, side_effect=RuntimeError("fail") + cog.join_handler, "process_member_join", new_callable=AsyncMock, side_effect=RuntimeError("fail") ): - await on_member_join_event(mock_member) + # Call the listener method directly + await cog.on_member_join(mock_member) mock_bot.log.error.assert_called_once() assert "Critical error" in mock_bot.log.error.call_args[0][0] @@ -823,7 +814,7 @@ async def test_on_member_join_event_handles_exception(self, mock_bot, mock_membe class TestOnMemberRemoveEvent: - """Test cases for the inner on_member_remove event function registered in OnMemberRemove.__init__.""" + """Test cases for the on_member_remove listener method in OnMemberRemove.""" @pytest.fixture def mock_bot(self): @@ -838,7 +829,6 @@ def mock_bot(self): bot.user.id = 99999 bot.user.avatar = MagicMock() bot.user.avatar.url = "https://example.com/bot.png" - bot.event = MagicMock(side_effect=lambda func: func) bot.add_cog = AsyncMock() return bot @@ -862,11 +852,10 @@ def mock_member(self): async def test_on_member_remove_event_calls_handler(self, mock_bot, mock_member): """Test that the on_member_remove event calls the leave handler.""" cog = OnMemberRemove(mock_bot) - # Get the registered event function - on_member_remove_event = mock_bot.event.call_args_list[0][0][0] - with patch.object(cog.leave_handler, 'process_member_leave', new_callable=AsyncMock) as mock_process: - await on_member_remove_event(mock_member) + with patch.object(cog.leave_handler, "process_member_leave", new_callable=AsyncMock) as mock_process: + # Call the listener method directly + await cog.on_member_remove(mock_member) mock_process.assert_called_once_with(mock_member) mock_bot.log.info.assert_called_once() assert "Member left" in mock_bot.log.info.call_args[0][0] @@ -875,11 +864,11 @@ async def test_on_member_remove_event_calls_handler(self, mock_bot, mock_member) async def test_on_member_remove_event_handles_exception(self, mock_bot, mock_member): """Test that on_member_remove catches exceptions and logs critical error.""" cog = OnMemberRemove(mock_bot) - on_member_remove_event = mock_bot.event.call_args_list[0][0][0] with patch.object( - cog.leave_handler, 'process_member_leave', new_callable=AsyncMock, side_effect=RuntimeError("fail") + cog.leave_handler, "process_member_leave", new_callable=AsyncMock, side_effect=RuntimeError("fail") ): - await on_member_remove_event(mock_member) + # Call the listener method directly + await cog.on_member_remove(mock_member) mock_bot.log.error.assert_called_once() assert "Critical error" in mock_bot.log.error.call_args[0][0] diff --git a/tests/unit/bot/events/test_on_command_error.py b/tests/unit/bot/events/test_on_command_error.py index 1677d113..0a0a8a19 100644 --- a/tests/unit/bot/events/test_on_command_error.py +++ b/tests/unit/bot/events/test_on_command_error.py @@ -6,7 +6,7 @@ from discord.ext import commands from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.events.on_command_error import ErrorContext, ErrorMessageBuilder, Errors from src.bot.constants import messages, variables @@ -202,10 +202,10 @@ def test_init(self, mock_bot): """Test Errors cog initialization.""" cog = Errors(mock_bot) assert cog.bot == mock_bot - assert hasattr(cog, 'message_builder') + assert hasattr(cog, "message_builder") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_send_error_message_no_log(self, mock_send_error, mock_ctx): """Test sending error message without logging.""" await Errors._send_error_message(mock_ctx, "Test error", False) @@ -214,7 +214,7 @@ async def test_send_error_message_no_log(self, mock_send_error, mock_ctx): mock_ctx.bot.log.error.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_send_error_message_with_log(self, mock_send_error, mock_ctx): """Test sending error message with logging.""" await Errors._send_error_message(mock_ctx, "Test error", True) @@ -228,7 +228,7 @@ async def test_send_error_message_with_log(self, mock_send_error, mock_ctx): assert "Test Server" in log_call @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_no_private_message(self, mock_send_error, errors_cog, mock_ctx): """Test handling NoPrivateMessage error.""" context = ErrorContext(mock_ctx, Exception()) @@ -238,7 +238,7 @@ async def test_handle_no_private_message(self, mock_send_error, errors_cog, mock mock_send_error.assert_called_once_with(mock_ctx, "Test error", True) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_command_not_found(self, mock_send_error, errors_cog, mock_ctx): """Test handling CommandNotFound error.""" context = ErrorContext(mock_ctx, Exception()) @@ -249,7 +249,7 @@ async def test_handle_command_not_found(self, mock_send_error, errors_cog, mock_ mock_send_error.assert_called_once_with(mock_ctx, expected_msg, False) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_bad_argument_gw2_config_status(self, mock_send_error, errors_cog, mock_ctx): """Test handling BadArgument error for GW2 config status.""" context = ErrorContext(mock_ctx, Exception()) @@ -262,8 +262,8 @@ async def test_handle_bad_argument_gw2_config_status(self, mock_send_error, erro mock_send_error.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') - @patch('src.bot.cogs.events.on_command_error.bot_utils.delete_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") + @patch("src.bot.cogs.events.on_command_error.bot_utils.delete_message") async def test_handle_command_on_cooldown_sensitive_command( self, mock_delete, mock_send_error, errors_cog, mock_ctx ): @@ -278,8 +278,8 @@ async def test_handle_command_on_cooldown_sensitive_command( mock_send_error.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') - @patch('src.bot.cogs.events.on_command_error.bot_utils.delete_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") + @patch("src.bot.cogs.events.on_command_error.bot_utils.delete_message") async def test_handle_command_on_cooldown_normal_command(self, mock_delete, mock_send_error, errors_cog, mock_ctx): """Test handling CommandOnCooldown for normal commands.""" context = ErrorContext(mock_ctx, Exception()) @@ -292,7 +292,7 @@ async def test_handle_command_on_cooldown_normal_command(self, mock_delete, mock mock_send_error.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_too_many_arguments(self, mock_send_error, errors_cog, mock_ctx): """Test handling TooManyArguments error.""" context = ErrorContext(mock_ctx, Exception()) @@ -303,7 +303,7 @@ async def test_handle_too_many_arguments(self, mock_send_error, errors_cog, mock mock_send_error.assert_called_once_with(mock_ctx, expected_msg, False) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_unknown_error(self, mock_send_error, errors_cog, mock_ctx): """Test handling unknown error types.""" context = ErrorContext(mock_ctx, Exception()) @@ -328,7 +328,7 @@ async def test_setup_function(self, mock_bot): def test_errors_cog_inheritance(self, errors_cog): """Test that Errors cog properly inherits from commands.Cog.""" assert isinstance(errors_cog, commands.Cog) - assert hasattr(errors_cog, 'bot') + assert hasattr(errors_cog, "bot") def test_error_context_with_complex_command(self, mock_ctx, mock_error): """Test ErrorContext with complex command structure.""" @@ -340,7 +340,7 @@ def test_error_context_with_complex_command(self, mock_ctx, mock_error): assert context.help_command == "!help admin config profanity on" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_command_error(self, mock_send_error, errors_cog, mock_ctx): """Test handling CommandError.""" context = ErrorContext(mock_ctx, Exception()) @@ -492,7 +492,7 @@ class TestOnCommandErrorEventHandler: """Test cases for the on_command_error event handler (lines 133-151).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_command_not_found(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches CommandNotFound properly (lines 133-151).""" cog = Errors(mock_bot) @@ -509,7 +509,7 @@ async def test_on_command_error_command_not_found(self, mock_send_error, mock_bo assert messages.COMMAND_NOT_FOUND in call_msg @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_missing_required_argument(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches MissingRequiredArgument (lines 140, 175-176).""" cog = Errors(mock_bot) @@ -526,7 +526,7 @@ async def test_on_command_error_missing_required_argument(self, mock_send_error, assert messages.MISSING_REQUIRED_ARGUMENT_HELP_MESSAGE in call_msg @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_check_failure(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches CheckFailure (lines 141, 180-181).""" cog = Errors(mock_bot) @@ -540,7 +540,7 @@ async def test_on_command_error_check_failure(self, mock_send_error, mock_bot, m assert messages.NOT_ADMIN_USE_COMMAND in call_msg @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_bad_argument_gw2_server(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches BadArgument for GW2 server (lines 142, 188-190).""" cog = Errors(mock_bot) @@ -554,7 +554,7 @@ async def test_on_command_error_bad_argument_gw2_server(self, mock_send_error, m mock_send_error.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_bad_argument_else_branch(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches BadArgument else branch (lines 191-192).""" cog = Errors(mock_bot) @@ -569,7 +569,7 @@ async def test_on_command_error_bad_argument_else_branch(self, mock_send_error, assert messages.UNKNOWN_OPTION in call_msg @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_command_invoke_error(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches CommandInvokeError (lines 144, 204-205).""" cog = Errors(mock_bot) @@ -584,7 +584,7 @@ async def test_on_command_error_command_invoke_error(self, mock_send_error, mock assert messages.COMMAND_INTERNAL_ERROR in call_msg @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_forbidden(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches discord.Forbidden (lines 147, 234-235).""" import discord @@ -603,7 +603,7 @@ async def test_on_command_error_forbidden(self, mock_send_error, mock_bot, mock_ assert call_msg == messages.DM_CANNOT_EXECUTE_COMMAND @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_forbidden_privilege_low(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches discord.Forbidden with low privilege (lines 234-235).""" import discord @@ -622,7 +622,7 @@ async def test_on_command_error_forbidden_privilege_low(self, mock_send_error, m assert call_msg == messages.PRIVILEGE_LOW @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_no_private_message(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches NoPrivateMessage (line 138).""" cog = Errors(mock_bot) @@ -634,7 +634,7 @@ async def test_on_command_error_no_private_message(self, mock_send_error, mock_b mock_send_error.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_command_error(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches CommandError (line 143).""" cog = Errors(mock_bot) @@ -648,7 +648,7 @@ async def test_on_command_error_command_error(self, mock_send_error, mock_bot, m assert "CommandError:" in call_msg @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_on_command_error_unknown_error_type(self, mock_send_error, mock_bot, mock_ctx): """Test on_command_error dispatches unknown error type (line 150).""" cog = Errors(mock_bot) @@ -664,7 +664,7 @@ class TestSendErrorMessageGuildNone: """Test cases for _send_error_message when guild is None (lines 204-205 logging branch).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_command_error.bot_utils.send_error_msg") async def test_send_error_message_with_log_no_guild(self, mock_send_error, mock_ctx): """Test sending error message with logging when guild is None.""" mock_ctx.guild = None @@ -684,7 +684,7 @@ class TestHandleBadArgumentBranches: """Test cases for _handle_bad_argument different branches (lines 188-192).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_bad_argument_gw2_config_server(self, mock_send_error, errors_cog, mock_ctx): """Test handling BadArgument for GW2 config server (lines 188-190).""" context = ErrorContext(mock_ctx, Exception()) @@ -699,7 +699,7 @@ async def test_handle_bad_argument_gw2_config_server(self, mock_send_error, erro mock_send_error.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_bad_argument_else_branch(self, mock_send_error, errors_cog, mock_ctx): """Test handling BadArgument else branch (lines 191-192).""" context = ErrorContext(mock_ctx, Exception()) @@ -716,7 +716,7 @@ class TestHandleMissingArgument: """Test cases for _handle_missing_argument (lines 175-176).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_missing_argument(self, mock_send_error, errors_cog, mock_ctx): """Test handling MissingRequiredArgument (lines 175-176).""" context = ErrorContext(mock_ctx, Exception()) @@ -732,7 +732,7 @@ class TestHandleCheckFailure: """Test cases for _handle_check_failure (lines 180-181).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_check_failure_not_owner(self, mock_send_error, errors_cog, mock_ctx): """Test handling CheckFailure with not owner (lines 180-181).""" context = ErrorContext(mock_ctx, Exception()) @@ -744,7 +744,7 @@ async def test_handle_check_failure_not_owner(self, mock_send_error, errors_cog, mock_send_error.assert_called_once_with(mock_ctx, expected_msg, True) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_check_failure_other(self, mock_send_error, errors_cog, mock_ctx): """Test handling CheckFailure with generic error (lines 180-181).""" context = ErrorContext(mock_ctx, Exception()) @@ -759,7 +759,7 @@ class TestHandleCommandInvokeError: """Test cases for _handle_command_invoke_error (lines 204-205).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_command_invoke_error(self, mock_send_error, errors_cog, mock_ctx): """Test handling CommandInvokeError (lines 204-205).""" context = ErrorContext(mock_ctx, Exception()) @@ -778,7 +778,7 @@ class TestHandleForbidden: """Test cases for _handle_forbidden (lines 234-235).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_forbidden_dm_channel(self, mock_send_error, errors_cog, mock_ctx): """Test handling Forbidden for DM channel (lines 234-235).""" context = ErrorContext(mock_ctx, Exception()) @@ -789,7 +789,7 @@ async def test_handle_forbidden_dm_channel(self, mock_send_error, errors_cog, mo mock_send_error.assert_called_once_with(mock_ctx, messages.DM_CANNOT_EXECUTE_COMMAND, True) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_command_error.Errors._send_error_message') + @patch("src.bot.cogs.events.on_command_error.Errors._send_error_message") async def test_handle_forbidden_privilege_low(self, mock_send_error, errors_cog, mock_ctx): """Test handling Forbidden for low privilege (lines 234-235).""" context = ErrorContext(mock_ctx, Exception()) diff --git a/tests/unit/bot/events/test_on_connect.py b/tests/unit/bot/events/test_on_connect.py index 43a8322e..1098db76 100644 --- a/tests/unit/bot/events/test_on_connect.py +++ b/tests/unit/bot/events/test_on_connect.py @@ -5,7 +5,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.events.on_connect import ConnectionHandler, GuildSynchronizer, OnConnect @@ -30,8 +30,6 @@ async def mock_fetch_guilds(limit=None): bot.fetch_guilds = mock_fetch_guilds # Ensure add_cog doesn't return a coroutine bot.add_cog = AsyncMock(return_value=None) - # Mock the event decorator to prevent coroutine issues - bot.event = MagicMock(side_effect=lambda func: func) return bot @@ -83,7 +81,7 @@ def test_init(self, mock_bot): assert synchronizer.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_connect.ServersDal') + @patch("src.bot.cogs.events.on_connect.ServersDal") async def test_get_database_server_ids_success(self, mock_dal_class, guild_synchronizer): """Test getting database server IDs successfully.""" # Setup mock @@ -97,7 +95,7 @@ async def test_get_database_server_ids_success(self, mock_dal_class, guild_synch mock_dal.get_server.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_connect.ServersDal') + @patch("src.bot.cogs.events.on_connect.ServersDal") async def test_get_database_server_ids_empty(self, mock_dal_class, guild_synchronizer): """Test getting database server IDs when empty.""" # Setup mock @@ -110,7 +108,7 @@ async def test_get_database_server_ids_empty(self, mock_dal_class, guild_synchro assert result == set() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_connect.ServersDal') + @patch("src.bot.cogs.events.on_connect.ServersDal") async def test_get_database_server_ids_error(self, mock_dal_class, guild_synchronizer): """Test getting database server IDs with error.""" # Setup mock to raise exception @@ -160,7 +158,7 @@ async def error_fetch_guilds(limit=None): guild_synchronizer.bot.log.error.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_connect.bot_utils.insert_server') + @patch("src.bot.cogs.events.on_connect.bot_utils.insert_server") async def test_add_missing_guilds_success(self, mock_insert_server, guild_synchronizer, mock_guild): """Test adding missing guilds successfully.""" missing_guild_ids = {12345} @@ -173,7 +171,7 @@ async def test_add_missing_guilds_success(self, mock_insert_server, guild_synchr guild_synchronizer.bot.log.info.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_connect.bot_utils.insert_server') + @patch("src.bot.cogs.events.on_connect.bot_utils.insert_server") async def test_add_missing_guilds_guild_not_found(self, mock_insert_server, guild_synchronizer): """Test adding missing guilds when guild not found.""" missing_guild_ids = {12345} @@ -186,7 +184,7 @@ async def test_add_missing_guilds_guild_not_found(self, mock_insert_server, guil guild_synchronizer.bot.log.warning.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_connect.bot_utils.insert_server') + @patch("src.bot.cogs.events.on_connect.bot_utils.insert_server") async def test_add_missing_guilds_insert_error(self, mock_insert_server, guild_synchronizer, mock_guild): """Test adding missing guilds with insert error.""" missing_guild_ids = {12345} @@ -297,10 +295,8 @@ async def test_on_connect_event_success(self, mock_bot): cog = OnConnect(mock_bot) cog.connection_handler.process_connection = AsyncMock() - # Access the event handler directly - on_connect_event = mock_bot.event.call_args_list[0][0][0] - - await on_connect_event() + # Call the listener method directly + await cog.on_connect() cog.connection_handler.process_connection.assert_called_once() @@ -310,10 +306,8 @@ async def test_on_connect_event_error(self, mock_bot): cog = OnConnect(mock_bot) cog.connection_handler.process_connection = AsyncMock(side_effect=RuntimeError("Critical error")) - # Access the event handler directly - on_connect_event = mock_bot.event.call_args_list[0][0][0] - - await on_connect_event() + # Call the listener method directly + await cog.on_connect() mock_bot.log.error.assert_called_once() error_call = mock_bot.log.error.call_args[0][0] @@ -324,7 +318,7 @@ def test_on_connect_cog_inheritance(self, on_connect_cog): from discord.ext import commands assert isinstance(on_connect_cog, commands.Cog) - assert hasattr(on_connect_cog, 'bot') + assert hasattr(on_connect_cog, "bot") @pytest.mark.asyncio async def test_guild_synchronizer_integration(self, mock_bot): @@ -333,13 +327,13 @@ async def test_guild_synchronizer_integration(self, mock_bot): cog = OnConnect(mock_bot) # Verify that connection handler has a guild synchronizer - assert hasattr(cog.connection_handler, 'guild_synchronizer') + assert hasattr(cog.connection_handler, "guild_synchronizer") assert isinstance(cog.connection_handler.guild_synchronizer, GuildSynchronizer) assert cog.connection_handler.guild_synchronizer.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_connect.ServersDal') - @patch('src.bot.cogs.events.on_connect.bot_utils.insert_server') + @patch("src.bot.cogs.events.on_connect.ServersDal") + @patch("src.bot.cogs.events.on_connect.bot_utils.insert_server") async def test_full_integration_sync_process(self, mock_insert_server, mock_dal_class, mock_bot, mock_guilds): """Test full integration of synchronization process.""" # Setup database mock diff --git a/tests/unit/bot/events/test_on_guild_join.py b/tests/unit/bot/events/test_on_guild_join.py index b8976a37..220a5b08 100644 --- a/tests/unit/bot/events/test_on_guild_join.py +++ b/tests/unit/bot/events/test_on_guild_join.py @@ -6,7 +6,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.events.on_guild_join import OnGuildJoin, WelcomeMessageBuilder from src.bot.constants import messages, variables @@ -28,8 +28,6 @@ def mock_bot(): bot.get_user = MagicMock() # Ensure add_cog doesn't return a coroutine bot.add_cog = AsyncMock(return_value=None) - # Mock the event decorator to prevent coroutine issues - bot.event = MagicMock(side_effect=lambda func: func) return bot @@ -77,7 +75,7 @@ def test_build_welcome_message(self, welcome_message_builder): result = welcome_message_builder.build_welcome_message(bot_name, prefix, games_included) # Should use the message template with proper formatting - expected = messages.GUILD_JOIN_BOT_MESSAGE.format(bot_name, prefix, games_included, prefix, prefix) + expected = messages.guild_join_bot_message(bot_name, prefix, games_included) assert result == expected def test_build_welcome_embed_with_avatar(self, welcome_message_builder, mock_bot): @@ -191,17 +189,15 @@ async def test_setup_function(self, mock_bot): assert added_cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_join.bot_utils.insert_server') - @patch('src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel') - @patch('src.bot.cogs.events.on_guild_join.variables.GAMES_INCLUDED', ['GW2', 'WoW']) + @patch("src.bot.cogs.events.on_guild_join.bot_utils.insert_server") + @patch("src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel") + @patch("src.bot.cogs.events.on_guild_join.variables.GAMES_INCLUDED", ["GW2", "WoW"]) async def test_on_guild_join_success(self, mock_send_msg, mock_insert_server, mock_bot, mock_guild): """Test successful guild join handling.""" - OnGuildJoin(mock_bot) - - # Access the event handler directly - on_guild_join_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildJoin(mock_bot) - await on_guild_join_event(mock_guild) + # Call the listener method directly + await cog.on_guild_join(mock_guild) # Verify server was inserted mock_insert_server.assert_called_once_with(mock_bot, mock_guild) @@ -217,17 +213,15 @@ async def test_on_guild_join_success(self, mock_send_msg, mock_insert_server, mo assert isinstance(call_args[3], str) # message text @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_join.bot_utils.insert_server') - @patch('src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel') - @patch('src.bot.cogs.events.on_guild_join.variables.GAMES_INCLUDED', ['GW2']) + @patch("src.bot.cogs.events.on_guild_join.bot_utils.insert_server") + @patch("src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel") + @patch("src.bot.cogs.events.on_guild_join.variables.GAMES_INCLUDED", ["GW2"]) async def test_on_guild_join_with_games(self, mock_send_msg, mock_insert_server, mock_bot, mock_guild): """Test guild join with specific games included.""" - OnGuildJoin(mock_bot) - - # Access the event handler directly - on_guild_join_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildJoin(mock_bot) - await on_guild_join_event(mock_guild) + # Call the listener method directly + await cog.on_guild_join(mock_guild) # Verify the welcome message includes games call_args = mock_send_msg.call_args[0] @@ -237,37 +231,31 @@ async def test_on_guild_join_with_games(self, mock_send_msg, mock_insert_server, assert "GW2" in welcome_text @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_join.bot_utils.insert_server') - @patch('src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_guild_join.bot_utils.insert_server") + @patch("src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel") async def test_on_guild_join_insert_server_error(self, mock_send_msg, mock_insert_server, mock_bot, mock_guild): """Test guild join when server insertion fails.""" mock_insert_server.side_effect = Exception("Database error") - OnGuildJoin(mock_bot) - - # Access the event handler directly - on_guild_join_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildJoin(mock_bot) # Should raise exception since we're not handling it with pytest.raises(Exception, match="Database error"): - await on_guild_join_event(mock_guild) + await cog.on_guild_join(mock_guild) # Insert should still have been attempted mock_insert_server.assert_called_once_with(mock_bot, mock_guild) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_join.bot_utils.insert_server') - @patch('src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_guild_join.bot_utils.insert_server") + @patch("src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel") async def test_on_guild_join_send_message_error(self, mock_send_msg, mock_insert_server, mock_bot, mock_guild): """Test guild join when sending message fails.""" mock_send_msg.side_effect = Exception("Send error") - OnGuildJoin(mock_bot) - - # Access the event handler directly - on_guild_join_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildJoin(mock_bot) # Should raise exception since we're not handling it with pytest.raises(Exception, match="Send error"): - await on_guild_join_event(mock_guild) + await cog.on_guild_join(mock_guild) # Both operations should have been attempted mock_insert_server.assert_called_once_with(mock_bot, mock_guild) @@ -278,19 +266,17 @@ def test_on_guild_join_cog_inheritance(self, on_guild_join_cog): from discord.ext import commands assert isinstance(on_guild_join_cog, commands.Cog) - assert hasattr(on_guild_join_cog, 'bot') + assert hasattr(on_guild_join_cog, "bot") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_join.bot_utils.insert_server') - @patch('src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_guild_join.bot_utils.insert_server") + @patch("src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel") async def test_on_guild_join_embed_properties(self, mock_send_msg, mock_insert_server, mock_bot, mock_guild): """Test that the welcome embed has the correct properties.""" - OnGuildJoin(mock_bot) - - # Access the event handler directly - on_guild_join_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildJoin(mock_bot) - await on_guild_join_event(mock_guild) + # Call the listener method directly + await cog.on_guild_join(mock_guild) # Get the embed that was sent call_args = mock_send_msg.call_args[0] @@ -311,17 +297,15 @@ def test_welcome_message_builder_static_methods(self): assert inspect.isfunction(WelcomeMessageBuilder._set_footer) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_join.bot_utils.insert_server') - @patch('src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel') - @patch('src.bot.cogs.events.on_guild_join.variables.GAMES_INCLUDED', []) + @patch("src.bot.cogs.events.on_guild_join.bot_utils.insert_server") + @patch("src.bot.cogs.events.on_guild_join.bot_utils.send_msg_to_system_channel") + @patch("src.bot.cogs.events.on_guild_join.variables.GAMES_INCLUDED", []) async def test_on_guild_join_no_games(self, mock_send_msg, mock_insert_server, mock_bot, mock_guild): """Test guild join with no games included.""" - OnGuildJoin(mock_bot) - - # Access the event handler directly - on_guild_join_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildJoin(mock_bot) - await on_guild_join_event(mock_guild) + # Call the listener method directly + await cog.on_guild_join(mock_guild) # Should still work with empty games list mock_insert_server.assert_called_once_with(mock_bot, mock_guild) diff --git a/tests/unit/bot/events/test_on_guild_update.py b/tests/unit/bot/events/test_on_guild_update.py index c1b5ad1c..cd80c479 100644 --- a/tests/unit/bot/events/test_on_guild_update.py +++ b/tests/unit/bot/events/test_on_guild_update.py @@ -5,7 +5,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.events.on_guild_update import OnGuildUpdate from src.bot.constants import messages @@ -24,8 +24,6 @@ def mock_bot(): bot.user.avatar.url = "https://example.com/bot_avatar.png" # Ensure add_cog doesn't return a coroutine bot.add_cog = AsyncMock(return_value=None) - # Mock the event decorator to prevent coroutine issues - bot.event = MagicMock(side_effect=lambda func: func) return bot @@ -95,10 +93,10 @@ async def test_setup_function(self, mock_bot): assert added_cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_guild_update.ServersDal') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_guild_update.ServersDal") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.send_msg_to_system_channel") async def test_on_guild_update_icon_change( self, mock_send_msg, @@ -127,10 +125,10 @@ async def test_on_guild_update_icon_change( mock_guild_before.name = mock_guild.name # Same name mock_guild_before.owner_id = mock_guild.owner_id # Same owner - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Verify embed setup mock_embed.set_footer.assert_called_with(icon_url=mock_bot.user.avatar.url, text="2023-01-01 12:00:00 UTC") @@ -143,10 +141,10 @@ async def test_on_guild_update_icon_change( mock_send_msg.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_guild_update.ServersDal') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_guild_update.ServersDal") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.send_msg_to_system_channel") async def test_on_guild_update_name_change( self, mock_send_msg, @@ -175,10 +173,10 @@ async def test_on_guild_update_name_change( mock_guild_before.icon.url = mock_guild.icon.url # Same icon mock_guild_before.owner_id = mock_guild.owner_id # Same owner - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Verify name change handling mock_embed.add_field.assert_any_call(name=messages.PREVIOUS_NAME, value="Old Test Server") @@ -188,10 +186,10 @@ async def test_on_guild_update_name_change( mock_send_msg.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_guild_update.ServersDal') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_guild_update.ServersDal") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.send_msg_to_system_channel") async def test_on_guild_update_owner_change( self, mock_send_msg, @@ -220,10 +218,10 @@ async def test_on_guild_update_owner_change( mock_guild_before.name = mock_guild.name # Same name mock_guild_before.icon.url = mock_guild.icon.url # Same icon - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Verify owner change handling mock_embed.set_thumbnail.assert_called_with(url=mock_guild.icon.url) @@ -234,9 +232,9 @@ async def test_on_guild_update_owner_change( mock_send_msg.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_guild_update.ServersDal') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_guild_update.ServersDal") async def test_on_guild_update_no_changes( self, mock_dal_class, mock_datetime, mock_get_embed, mock_bot, mock_guild_before, mock_guild, mock_embed ): @@ -257,19 +255,19 @@ async def test_on_guild_update_no_changes( # Mock empty fields list mock_embed.fields = [] - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Should not send a message if no changes mock_dal.get_server.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_guild_update.ServersDal') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_guild_update.ServersDal") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.send_msg_to_system_channel") async def test_on_guild_update_disabled_notifications( self, mock_send_msg, @@ -299,18 +297,18 @@ async def test_on_guild_update_disabled_notifications( # Mock fields being added mock_embed.fields = [MagicMock()] - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Should not send message if notifications disabled mock_send_msg.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_guild_update.ServersDal') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_guild_update.ServersDal") async def test_on_guild_update_no_server_config( self, mock_dal_class, @@ -339,17 +337,17 @@ async def test_on_guild_update_no_server_config( # Mock fields being added mock_embed.fields = [MagicMock()] - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Should get server config but not send message mock_dal.get_server.assert_called_once_with(mock_guild.id) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") async def test_on_guild_update_bot_no_avatar( self, mock_datetime, @@ -370,18 +368,18 @@ async def test_on_guild_update_bot_no_avatar( mock_guild_before.icon.url = mock_guild.icon.url mock_guild_before.owner_id = mock_guild.owner_id - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Should set footer with None icon_url mock_embed.set_footer.assert_called_with(icon_url=None, text="2023-01-01 12:00:00 UTC") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_guild_update.ServersDal') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_guild_update.ServersDal") async def test_on_guild_update_database_error( self, mock_dal_class, @@ -410,10 +408,10 @@ async def test_on_guild_update_database_error( # Mock fields being added mock_embed.fields = [MagicMock()] - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Should log error mock_bot.log.error.assert_called() @@ -421,16 +419,16 @@ async def test_on_guild_update_database_error( assert "Failed to send guild update notification" in error_call @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") async def test_on_guild_update_general_error(self, mock_get_embed, mock_bot, mock_guild_before, mock_guild): """Test on_guild_update with general error.""" # Setup embed to raise exception mock_get_embed.side_effect = Exception("General error") - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Should log error mock_bot.log.error.assert_called() @@ -439,10 +437,10 @@ async def test_on_guild_update_general_error(self, mock_get_embed, mock_bot, moc assert mock_guild.name in error_call @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_guild_update.ServersDal') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_guild_update.ServersDal") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.send_msg_to_system_channel") async def test_on_guild_update_icon_url_change( self, mock_send_msg, @@ -475,18 +473,18 @@ async def test_on_guild_update_icon_url_change( # Mock fields being added mock_embed.fields = [MagicMock()] - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Should handle icon change mock_embed.add_field.assert_called_with(name=messages.NEW_SERVER_ICON, value="") mock_send_msg.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") async def test_on_guild_update_null_name_before( self, mock_datetime, mock_get_embed, mock_bot, mock_guild_before, mock_guild, mock_embed ): @@ -502,17 +500,17 @@ async def test_on_guild_update_null_name_before( mock_guild_before.icon.url = mock_guild.icon.url mock_guild_before.owner_id = mock_guild.owner_id - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Should only add new name field (not previous) mock_embed.add_field.assert_called_with(name=messages.NEW_SERVER_NAME, value="New Test Server") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_guild_update.bot_utils.get_current_date_time_str_long") async def test_on_guild_update_null_owner_before( self, mock_datetime, @@ -534,10 +532,10 @@ async def test_on_guild_update_null_owner_before( mock_guild_before.name = mock_guild.name mock_guild_before.icon.url = mock_guild.icon.url - OnGuildUpdate(mock_bot) - on_guild_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnGuildUpdate(mock_bot) - await on_guild_update_event(mock_guild_before, mock_guild) + # Call the listener method directly + await cog.on_guild_update(mock_guild_before, mock_guild) # Should only add new owner field (not previous) mock_embed.add_field.assert_called_with(name=messages.NEW_SERVER_OWNER, value=str(mock_guild.owner)) diff --git a/tests/unit/bot/events/test_on_member_update.py b/tests/unit/bot/events/test_on_member_update.py index 23ab803f..f783d73e 100644 --- a/tests/unit/bot/events/test_on_member_update.py +++ b/tests/unit/bot/events/test_on_member_update.py @@ -5,7 +5,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.events.on_member_update import OnMemberUpdate from src.bot.constants import messages @@ -24,8 +24,6 @@ def mock_bot(): bot.user.avatar.url = "https://example.com/bot_avatar.png" # Ensure add_cog doesn't return a coroutine bot.add_cog = AsyncMock(return_value=None) - # Mock the event decorator to prevent coroutine issues - bot.event = MagicMock(side_effect=lambda func: func) return bot @@ -111,10 +109,10 @@ async def test_setup_function(self, mock_bot): assert added_cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_member_update.ServersDal') - @patch('src.bot.cogs.events.on_member_update.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_member_update.ServersDal") + @patch("src.bot.cogs.events.on_member_update.bot_utils.send_msg_to_system_channel") async def test_on_member_update_bot_member( self, mock_send_msg, @@ -127,22 +125,20 @@ async def test_on_member_update_bot_member( ): """Test on_member_update with bot member (should be skipped).""" mock_member.bot = True - OnMemberUpdate(mock_bot) - - # Access the event handler directly - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Should not process bot members mock_get_embed.assert_not_called() mock_send_msg.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_member_update.ServersDal') - @patch('src.bot.cogs.events.on_member_update.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_member_update.ServersDal") + @patch("src.bot.cogs.events.on_member_update.bot_utils.send_msg_to_system_channel") async def test_on_member_update_nickname_change( self, mock_send_msg, @@ -171,10 +167,10 @@ async def test_on_member_update_nickname_change( # Mock embed fields to simulate having fields after add_field is called mock_embed.fields = [MagicMock()] # Simulate one field added - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Verify embed setup mock_embed.set_author.assert_called_with(name=mock_member.display_name, icon_url=mock_member.avatar.url) @@ -188,10 +184,10 @@ async def test_on_member_update_nickname_change( mock_send_msg.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_member_update.ServersDal') - @patch('src.bot.cogs.events.on_member_update.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_member_update.ServersDal") + @patch("src.bot.cogs.events.on_member_update.bot_utils.send_msg_to_system_channel") async def test_on_member_update_role_change( self, mock_send_msg, @@ -227,10 +223,10 @@ async def test_on_member_update_role_change( # Mock embed fields to simulate having fields after add_field is called mock_embed.fields = [MagicMock()] # Simulate one field added - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Verify role fields added mock_embed.add_field.assert_any_call(name=messages.PREVIOUS_ROLES, value="Member") @@ -240,9 +236,9 @@ async def test_on_member_update_role_change( mock_send_msg.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_member_update.ServersDal') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_member_update.ServersDal") async def test_on_member_update_no_changes( self, mock_dal_class, @@ -269,19 +265,19 @@ async def test_on_member_update_no_changes( # Mock empty fields list mock_embed.fields = [] - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Should not send message if no changes mock_dal.get_server.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_member_update.ServersDal') - @patch('src.bot.cogs.events.on_member_update.bot_utils.send_msg_to_system_channel') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_member_update.ServersDal") + @patch("src.bot.cogs.events.on_member_update.bot_utils.send_msg_to_system_channel") async def test_on_member_update_disabled_notifications( self, mock_send_msg, @@ -310,18 +306,18 @@ async def test_on_member_update_disabled_notifications( # Mock fields being added mock_embed.fields = [MagicMock()] - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Should not send message if notifications disabled mock_send_msg.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_member_update.ServersDal') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_member_update.ServersDal") async def test_on_member_update_no_server_config( self, mock_dal_class, @@ -349,17 +345,17 @@ async def test_on_member_update_no_server_config( # Mock fields being added mock_embed.fields = [MagicMock()] - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Should get server config but not send message mock_dal.get_server.assert_called_once_with(mock_member.guild.id) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") async def test_on_member_update_no_avatar( self, mock_datetime, @@ -379,17 +375,17 @@ async def test_on_member_update_no_avatar( mock_member.nick = "NewNick" mock_member_before.roles = mock_member.roles - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Should set author without icon_url mock_embed.set_author.assert_called_with(name=mock_member.display_name) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") async def test_on_member_update_bot_no_avatar( self, mock_datetime, @@ -409,18 +405,18 @@ async def test_on_member_update_bot_no_avatar( mock_member.nick = "NewNick" mock_member_before.roles = mock_member.roles - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Should set footer with None icon_url mock_embed.set_footer.assert_called_with(icon_url=None, text="2023-01-01 12:00:00 UTC") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') - @patch('src.bot.cogs.events.on_member_update.ServersDal') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") + @patch("src.bot.cogs.events.on_member_update.ServersDal") async def test_on_member_update_database_error( self, mock_dal_class, @@ -448,10 +444,10 @@ async def test_on_member_update_database_error( # Mock fields being added mock_embed.fields = [MagicMock()] - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Should log error mock_bot.log.error.assert_called() @@ -459,16 +455,16 @@ async def test_on_member_update_database_error( assert "Failed to send member update notification" in error_call @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") async def test_on_member_update_general_error(self, mock_get_embed, mock_bot, mock_member_before, mock_member): """Test on_member_update with general error.""" # Setup embed to raise exception mock_get_embed.side_effect = Exception("General error") - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Should log error mock_bot.log.error.assert_called() @@ -477,8 +473,8 @@ async def test_on_member_update_general_error(self, mock_get_embed, mock_bot, mo assert str(mock_member) in error_call @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") async def test_on_member_update_null_nick_to_nick( self, mock_datetime, @@ -497,17 +493,17 @@ async def test_on_member_update_null_nick_to_nick( mock_member.nick = "NewNick" mock_member_before.roles = mock_member.roles - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Should only add new nickname field (not previous) mock_embed.add_field.assert_called_with(name=messages.NEW_NICKNAME, value="NewNick") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_embed') - @patch('src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long') + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_embed") + @patch("src.bot.cogs.events.on_member_update.bot_utils.get_current_date_time_str_long") async def test_on_member_update_null_roles( self, mock_datetime, @@ -528,10 +524,10 @@ async def test_on_member_update_null_roles( mock_member.roles = [role1] mock_member_before.nick = mock_member.nick - OnMemberUpdate(mock_bot) - on_member_update_event = mock_bot.event.call_args_list[0][0][0] + cog = OnMemberUpdate(mock_bot) - await on_member_update_event(mock_member_before, mock_member) + # Call the listener method directly + await cog.on_member_update(mock_member_before, mock_member) # Should only add new roles field (not previous) mock_embed.add_field.assert_called_with(name=messages.NEW_ROLES, value="Member") diff --git a/tests/unit/bot/events/test_on_message.py b/tests/unit/bot/events/test_on_message.py index 55f91153..20e163f1 100644 --- a/tests/unit/bot/events/test_on_message.py +++ b/tests/unit/bot/events/test_on_message.py @@ -7,7 +7,7 @@ from discord.ext import commands from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.events.on_message import ( CustomReactionHandler, @@ -218,7 +218,7 @@ async def test_check_and_censor_no_profanity(self, mock_bot, mock_ctx): mock_bot.profanity.contains_profanity.assert_called_once_with("Hello world") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.delete_message') + @patch("src.bot.cogs.events.on_message.bot_utils.delete_message") async def test_check_and_censor_with_profanity(self, mock_delete, mock_bot, mock_ctx): """Test check_and_censor with profanity.""" mock_bot.profanity.contains_profanity.return_value = True @@ -354,7 +354,7 @@ async def test_check_exclusive_users_allowed(self, mock_bot, mock_ctx): assert result is True @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_message.bot_utils.send_error_msg") async def test_check_exclusive_users_not_allowed(self, mock_send_error, mock_bot, mock_ctx): """Test exclusive users check when user is not allowed.""" mock_bot.settings["bot"]["ExclusiveUsers"] = [99999] @@ -372,10 +372,10 @@ def test_init(self, mock_bot): """Test DMMessageHandler initialization.""" handler = DMMessageHandler(mock_bot) assert handler.bot == mock_bot - assert hasattr(handler, 'reaction_handler') + assert hasattr(handler, "reaction_handler") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.is_bot_owner') + @patch("src.bot.cogs.events.on_message.bot_utils.is_bot_owner") async def test_handle_dm_non_command_owner(self, mock_is_owner, mock_bot, mock_ctx): """Test DM non-command handling for bot owner.""" mock_is_owner.return_value = True @@ -393,7 +393,7 @@ async def test_handle_dm_non_command_owner(self, mock_is_owner, mock_bot, mock_c mock_ctx.author.send.assert_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.is_bot_owner') + @patch("src.bot.cogs.events.on_message.bot_utils.is_bot_owner") async def test_handle_dm_non_command_not_owner(self, mock_is_owner, mock_bot, mock_ctx): """Test DM non-command handling for non-owner.""" mock_is_owner.return_value = False @@ -435,11 +435,11 @@ def test_init(self, mock_bot): """Test ServerMessageHandler initialization.""" handler = ServerMessageHandler(mock_bot) assert handler.bot == mock_bot - assert hasattr(handler, 'profanity_filter') - assert hasattr(handler, 'reaction_handler') + assert hasattr(handler, "profanity_filter") + assert hasattr(handler, "reaction_handler") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ServersDal') + @patch("src.bot.cogs.events.on_message.ServersDal") async def test_process_no_configs(self, mock_dal_class, mock_bot, mock_ctx): """Test server message processing with no configs.""" mock_dal = AsyncMock() @@ -454,8 +454,8 @@ async def test_process_no_configs(self, mock_dal_class, mock_bot, mock_ctx): mock_bot.process_commands.assert_called_once_with(mock_ctx.message) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ServersDal') - @patch('src.bot.cogs.events.on_message.bot_utils.delete_message') + @patch("src.bot.cogs.events.on_message.ServersDal") + @patch("src.bot.cogs.events.on_message.bot_utils.delete_message") async def test_process_invisible_member_blocked(self, mock_delete, mock_dal_class, mock_bot, mock_ctx): """Test server message processing with invisible member blocked.""" mock_dal = AsyncMock() @@ -474,7 +474,7 @@ async def test_process_invisible_member_blocked(self, mock_delete, mock_dal_clas mock_delete.assert_called_once_with(mock_ctx) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ServersDal') + @patch("src.bot.cogs.events.on_message.ServersDal") async def test_handle_server_non_command_profanity(self, mock_dal_class, mock_bot, mock_ctx): """Test handling server non-command with profanity filter.""" configs = {"profanity_filter": True, "bot_word_reactions": False} @@ -486,7 +486,7 @@ async def test_handle_server_non_command_profanity(self, mock_dal_class, mock_bo handler.profanity_filter.check_and_censor.assert_called_once_with(mock_ctx) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.CustomCommandsDal') + @patch("src.bot.cogs.events.on_message.CustomCommandsDal") async def test_try_custom_command_found(self, mock_dal_class, mock_bot, mock_ctx): """Test trying custom command when found.""" mock_dal = AsyncMock() @@ -502,7 +502,7 @@ async def test_try_custom_command_found(self, mock_dal_class, mock_bot, mock_ctx mock_ctx.message.channel.send.assert_called_once_with("Custom command response") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.CustomCommandsDal') + @patch("src.bot.cogs.events.on_message.CustomCommandsDal") async def test_try_custom_command_not_found(self, mock_dal_class, mock_bot, mock_ctx): """Test trying custom command when not found.""" mock_dal = AsyncMock() @@ -523,8 +523,8 @@ def test_init(self, mock_bot): """Test OnMessage cog initialization.""" cog = OnMessage(mock_bot) assert cog.bot == mock_bot - assert hasattr(cog, 'dm_handler') - assert hasattr(cog, 'server_handler') + assert hasattr(cog, "dm_handler") + assert hasattr(cog, "server_handler") @pytest.mark.asyncio async def test_setup_function(self, mock_bot): @@ -541,7 +541,7 @@ async def test_setup_function(self, mock_bot): def test_on_message_cog_inheritance(self, on_message_cog): """Test that OnMessage cog properly inherits from commands.Cog.""" assert isinstance(on_message_cog, commands.Cog) - assert hasattr(on_message_cog, 'bot') + assert hasattr(on_message_cog, "bot") def test_message_context_dm_detection(self, mock_dm_ctx): """Test MessageContext DM detection.""" diff --git a/tests/unit/bot/events/test_on_message_extra.py b/tests/unit/bot/events/test_on_message_extra.py index 6df7dd70..b3dfe275 100644 --- a/tests/unit/bot/events/test_on_message_extra.py +++ b/tests/unit/bot/events/test_on_message_extra.py @@ -9,7 +9,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.events.on_message import ( DMMessageHandler, @@ -105,7 +105,7 @@ class TestProfanityFilterHTTPException: """Tests for ProfanityFilter._censor_message HTTPException fallback (lines 83-84).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.delete_message') + @patch("src.bot.cogs.events.on_message.bot_utils.delete_message") async def test_censor_message_embed_http_exception_fallback(self, mock_delete, mock_bot, mock_ctx): """When embed send raises HTTPException, fallback to text mention (lines 83-84).""" mock_bot.profanity.contains_profanity.return_value = True @@ -141,7 +141,7 @@ class TestDMMessageHandlerProcess: """Tests for DMMessageHandler.process routing (lines 198-201).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.is_bot_owner') + @patch("src.bot.cogs.events.on_message.bot_utils.is_bot_owner") async def test_process_not_command_routes_to_non_command(self, mock_is_owner, mock_bot, mock_dm_ctx): """When is_command=False, routes to _handle_dm_non_command (lines 198-199).""" mock_is_owner.return_value = False @@ -181,7 +181,7 @@ async def test_reaction_handler_returns_true_early_return(self, mock_bot, mock_d mock_dm_ctx.message.author.send.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.is_bot_owner') + @patch("src.bot.cogs.events.on_message.bot_utils.is_bot_owner") async def test_is_bot_owner_sends_owner_help(self, mock_is_owner, mock_bot, mock_dm_ctx): """When user is bot owner, sends owner help (lines 209-210).""" mock_is_owner.return_value = True @@ -199,7 +199,7 @@ async def test_is_bot_owner_sends_owner_help(self, mock_is_owner, mock_bot, mock assert mock_dm_ctx.message.author.send.call_count >= 1 @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.is_bot_owner') + @patch("src.bot.cogs.events.on_message.bot_utils.is_bot_owner") async def test_not_bot_owner_sends_dm_not_allowed(self, mock_is_owner, mock_bot, mock_dm_ctx): """When user is not bot owner, sends dm not allowed (lines 211-212).""" mock_is_owner.return_value = False @@ -210,7 +210,7 @@ async def test_not_bot_owner_sends_dm_not_allowed(self, mock_is_owner, mock_bot, mock_dm_ctx.message.author.send.assert_called_once() embed_sent = ( - mock_dm_ctx.message.author.send.call_args[1].get('embed') or mock_dm_ctx.message.author.send.call_args[0][0] + mock_dm_ctx.message.author.send.call_args[1].get("embed") or mock_dm_ctx.message.author.send.call_args[0][0] if mock_dm_ctx.message.author.send.call_args[0] else None ) @@ -221,7 +221,7 @@ class TestDMMessageHandlerCommand: """Tests for DMMessageHandler._handle_dm_command (lines 214-230).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ExclusiveUsersChecker.check_exclusive_users') + @patch("src.bot.cogs.events.on_message.ExclusiveUsersChecker.check_exclusive_users") async def test_exclusive_users_check_fails_returns(self, mock_check, mock_bot, mock_dm_ctx): """When exclusive users check fails, returns early (line 217).""" mock_check.return_value = False @@ -244,7 +244,7 @@ async def test_allowed_commands_none_sends_no_dm_commands(self, mock_bot, mock_d mock_dm_ctx.message.author.send.assert_called_once() embed_arg = ( - mock_dm_ctx.message.author.send.call_args[1].get('embed') or mock_dm_ctx.message.author.send.call_args[0][0] + mock_dm_ctx.message.author.send.call_args[1].get("embed") or mock_dm_ctx.message.author.send.call_args[0][0] ) assert isinstance(embed_arg, discord.Embed) mock_bot.process_commands.assert_not_called() @@ -284,7 +284,7 @@ async def test_send_no_dm_commands_allowed(self, mock_bot, mock_dm_ctx): await handler._send_no_dm_commands_allowed(mock_dm_ctx) mock_dm_ctx.message.author.send.assert_called_once() - embed_sent = mock_dm_ctx.message.author.send.call_args[1]['embed'] + embed_sent = mock_dm_ctx.message.author.send.call_args[1]["embed"] assert embed_sent.color == discord.Color.red() assert messages.DM_COMMAND_NOT_ALLOWED in embed_sent.description @@ -297,7 +297,7 @@ async def test_send_command_not_allowed(self, mock_bot, mock_dm_ctx): await handler._send_command_not_allowed(mock_dm_ctx, allowed_commands) mock_dm_ctx.message.author.send.assert_called_once() - embed_sent = mock_dm_ctx.message.author.send.call_args[1]['embed'] + embed_sent = mock_dm_ctx.message.author.send.call_args[1]["embed"] assert embed_sent.color == discord.Color.red() assert messages.DM_COMMAND_NOT_ALLOWED in embed_sent.description assert len(embed_sent.fields) == 1 @@ -311,7 +311,7 @@ async def test_send_dm_not_allowed(self, mock_bot, mock_dm_ctx): await handler._send_dm_not_allowed(mock_dm_ctx) mock_dm_ctx.message.author.send.assert_called_once() - embed_sent = mock_dm_ctx.message.author.send.call_args[1]['embed'] + embed_sent = mock_dm_ctx.message.author.send.call_args[1]["embed"] assert embed_sent.color == discord.Color.red() assert messages.NO_DM_MESSAGES in embed_sent.description @@ -335,7 +335,7 @@ async def test_send_owner_help(self, mock_bot, mock_dm_ctx): # Second call: box(owner_command.help) assert mock_dm_ctx.message.author.send.call_count == 2 first_call = mock_dm_ctx.message.author.send.call_args_list[0] - embed_sent = first_call[1]['embed'] + embed_sent = first_call[1]["embed"] assert embed_sent.color == discord.Color.green() assert messages.OWNER_DM_BOT_MESSAGE in embed_sent.description second_call = mock_dm_ctx.message.author.send.call_args_list[1] @@ -346,7 +346,7 @@ class TestServerMessageHandlerProcess: """Tests for ServerMessageHandler.process (lines 281-301, specifically 298-301).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ServersDal') + @patch("src.bot.cogs.events.on_message.ServersDal") async def test_no_configs_not_command_logs_warning_no_process(self, mock_dal_class, mock_bot, mock_ctx): """When no configs and not a command, logs warning and returns (lines 288-291).""" mock_dal = AsyncMock() @@ -360,7 +360,7 @@ async def test_no_configs_not_command_logs_warning_no_process(self, mock_dal_cla mock_bot.process_commands.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ServersDal') + @patch("src.bot.cogs.events.on_message.ServersDal") async def test_no_configs_is_command_processes_commands(self, mock_dal_class, mock_bot, mock_ctx): """When no configs but is_command, processes commands (lines 289-290, 298-301 related).""" mock_dal = AsyncMock() @@ -374,7 +374,7 @@ async def test_no_configs_is_command_processes_commands(self, mock_dal_class, mo mock_bot.process_commands.assert_called_once_with(mock_ctx.message) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ServersDal') + @patch("src.bot.cogs.events.on_message.ServersDal") async def test_not_command_routes_to_non_command(self, mock_dal_class, mock_bot, mock_ctx): """When not a command, routes to _handle_server_non_command (lines 298-299).""" mock_dal = AsyncMock() @@ -392,8 +392,8 @@ async def test_not_command_routes_to_non_command(self, mock_dal_class, mock_bot, mock_bot.process_commands.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ServersDal') - @patch('src.bot.cogs.events.on_message.CustomCommandsDal') + @patch("src.bot.cogs.events.on_message.ServersDal") + @patch("src.bot.cogs.events.on_message.CustomCommandsDal") async def test_is_command_routes_to_command_handler(self, mock_cmd_dal_class, mock_dal_class, mock_bot, mock_ctx): """When is a command, routes to _handle_server_command (lines 300-301).""" mock_dal = AsyncMock() @@ -419,7 +419,7 @@ class TestServerMessageHandlerInvisibleMember: """Tests for ServerMessageHandler._handle_invisible_member (lines 303-322).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.delete_message') + @patch("src.bot.cogs.events.on_message.bot_utils.delete_message") async def test_invisible_member_dm_succeeds(self, mock_delete, mock_bot, mock_ctx): """When DM send succeeds, invisible member is notified via DM (lines 316-317).""" handler = ServerMessageHandler(mock_bot) @@ -428,11 +428,11 @@ async def test_invisible_member_dm_succeeds(self, mock_delete, mock_bot, mock_ct mock_delete.assert_called_once_with(mock_ctx) mock_ctx.message.author.send.assert_called_once() - embed_sent = mock_ctx.message.author.send.call_args[1]['embed'] + embed_sent = mock_ctx.message.author.send.call_args[1]["embed"] assert embed_sent.color == discord.Color.red() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.delete_message') + @patch("src.bot.cogs.events.on_message.bot_utils.delete_message") async def test_invisible_member_dm_http_exception_channel_send(self, mock_delete, mock_bot, mock_ctx): """When DM raises HTTPException, tries channel send (lines 318-319).""" handler = ServerMessageHandler(mock_bot) @@ -444,11 +444,11 @@ async def test_invisible_member_dm_http_exception_channel_send(self, mock_delete mock_ctx.message.author.send.assert_called_once() # Falls back to ctx.send with embed mock_ctx.send.assert_called_once() - embed_sent = mock_ctx.send.call_args[1]['embed'] + embed_sent = mock_ctx.send.call_args[1]["embed"] assert embed_sent.color == discord.Color.red() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.delete_message') + @patch("src.bot.cogs.events.on_message.bot_utils.delete_message") async def test_invisible_member_both_exceptions_fallback_mention(self, mock_delete, mock_bot, mock_ctx): """When both DM and channel embed raise HTTPException, fallback to mention (lines 320-322).""" handler = ServerMessageHandler(mock_bot) @@ -458,7 +458,7 @@ async def test_invisible_member_both_exceptions_fallback_mention(self, mock_dele async def send_side_effect(*args, **kwargs): call_count[0] += 1 - if call_count[0] == 1 and 'embed' in kwargs: + if call_count[0] == 1 and "embed" in kwargs: raise discord.HTTPException(MagicMock(), "Channel embed failed") return MagicMock() @@ -542,7 +542,7 @@ async def test_double_prefix_returns_early(self, mock_bot, mock_ctx): mock_bot.process_commands.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ExclusiveUsersChecker.check_exclusive_users') + @patch("src.bot.cogs.events.on_message.ExclusiveUsersChecker.check_exclusive_users") async def test_exclusive_users_fails_returns(self, mock_check, mock_bot, mock_ctx): """When exclusive users check fails, returns early (lines 341-342).""" mock_check.return_value = False @@ -555,7 +555,7 @@ async def test_exclusive_users_fails_returns(self, mock_check, mock_bot, mock_ct mock_bot.process_commands.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.CustomCommandsDal') + @patch("src.bot.cogs.events.on_message.CustomCommandsDal") async def test_custom_command_found_returns(self, mock_dal_class, mock_bot, mock_ctx): """When custom command is found, returns True (lines 345-347).""" mock_dal = AsyncMock() @@ -572,7 +572,7 @@ async def test_custom_command_found_returns(self, mock_dal_class, mock_bot, mock mock_bot.process_commands.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.CustomCommandsDal') + @patch("src.bot.cogs.events.on_message.CustomCommandsDal") async def test_no_custom_command_processes_regular(self, mock_dal_class, mock_bot, mock_ctx): """When no custom command found, processes regular commands (line 350).""" mock_dal = AsyncMock() @@ -597,7 +597,7 @@ async def test_non_alpha_second_char_double_prefix(self, mock_bot, mock_ctx): mock_bot.process_commands.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.CustomCommandsDal') + @patch("src.bot.cogs.events.on_message.CustomCommandsDal") async def test_alpha_second_char_not_double_prefix(self, mock_dal_class, mock_bot, mock_ctx): """Alpha character after prefix is NOT double prefix (line 338).""" mock_dal = AsyncMock() @@ -620,8 +620,8 @@ def test_on_message_cog_registers_event(self, mock_bot): cog = OnMessage(mock_bot) assert cog.bot == mock_bot - assert hasattr(cog, 'dm_handler') - assert hasattr(cog, 'server_handler') + assert hasattr(cog, "dm_handler") + assert hasattr(cog, "server_handler") mock_bot.event.assert_called_once() @pytest.mark.asyncio @@ -735,8 +735,8 @@ class TestServerMessageHandlerBlockInvisible: """Additional tests for block invisible member flow (lines 294-296).""" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ServersDal') - @patch('src.bot.cogs.events.on_message.bot_utils.delete_message') + @patch("src.bot.cogs.events.on_message.ServersDal") + @patch("src.bot.cogs.events.on_message.bot_utils.delete_message") async def test_block_invisible_enabled_member_invisible(self, mock_delete, mock_dal_class, mock_bot, mock_ctx): """When block invisible is True and member is invisible, handles invisible (lines 294-296).""" mock_dal = AsyncMock() @@ -756,8 +756,8 @@ async def test_block_invisible_enabled_member_invisible(self, mock_delete, mock_ mock_bot.process_commands.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.ServersDal') - @patch('src.bot.cogs.events.on_message.CustomCommandsDal') + @patch("src.bot.cogs.events.on_message.ServersDal") + @patch("src.bot.cogs.events.on_message.CustomCommandsDal") async def test_block_invisible_enabled_member_online(self, mock_cmd_dal_class, mock_dal_class, mock_bot, mock_ctx): """When block invisible is True but member is online, proceeds normally.""" mock_dal = AsyncMock() @@ -801,7 +801,7 @@ async def test_exclusive_users_single_id_match(self, mock_bot, mock_ctx): assert result is True @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_message.bot_utils.send_error_msg') + @patch("src.bot.cogs.events.on_message.bot_utils.send_error_msg") async def test_exclusive_users_single_id_no_match(self, mock_send_error, mock_bot, mock_ctx): """When ExclusiveUsers is a single int not matching user, returns False.""" mock_bot.settings["bot"]["ExclusiveUsers"] = 11111 diff --git a/tests/unit/bot/events/test_on_ready.py b/tests/unit/bot/events/test_on_ready.py index e6b59f98..8e62497f 100644 --- a/tests/unit/bot/events/test_on_ready.py +++ b/tests/unit/bot/events/test_on_ready.py @@ -5,7 +5,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.cogs.events.on_ready import OnReady, StartupInfoDisplay from src.bot.constants import messages, variables @@ -24,8 +24,6 @@ def mock_bot(): bot.command_prefix = "!" # Ensure add_cog doesn't return a coroutine bot.add_cog = AsyncMock(return_value=None) - # Mock the event decorator to prevent coroutine issues - bot.event = MagicMock(side_effect=lambda func: func) return bot @@ -44,13 +42,13 @@ def startup_info_display(): @pytest.fixture def mock_bot_stats(): """Create mock bot statistics.""" - return {'servers': 5, 'users': 150, 'channels': 45} + return {"servers": 5, "users": 150, "channels": 45} class TestStartupInfoDisplay: """Test cases for StartupInfoDisplay class.""" - @patch('builtins.print') + @patch("builtins.print") def test_print_startup_banner(self, mock_print, startup_info_display): """Test printing startup banner.""" test_version = "2.0.21" @@ -64,8 +62,8 @@ def test_print_startup_banner(self, mock_print, startup_info_display): assert "=" * 20 in printed_text assert f"Discord Bot v{test_version}" in printed_text - @patch('builtins.print') - @patch('discord.__version__', '2.3.2') + @patch("builtins.print") + @patch("discord.__version__", "2.3.2") def test_print_version_info(self, mock_print, startup_info_display): """Test printing version information.""" startup_info_display.print_version_info() @@ -81,7 +79,7 @@ def test_print_version_info(self, mock_print, startup_info_display): # Check Discord version assert calls[1] == "Discord API v2.3.2" - @patch('builtins.print') + @patch("builtins.print") def test_print_bot_info(self, mock_print, startup_info_display, mock_bot): """Test printing bot information.""" startup_info_display.print_bot_info(mock_bot) @@ -95,7 +93,7 @@ def test_print_bot_info(self, mock_print, startup_info_display, mock_bot): assert "(id:123456789)" in calls[1] assert calls[2] == "Prefix: !" - @patch('builtins.print') + @patch("builtins.print") def test_print_bot_stats(self, mock_print, startup_info_display, mock_bot_stats): """Test printing bot statistics.""" startup_info_display.print_bot_stats(mock_bot_stats) @@ -108,8 +106,8 @@ def test_print_bot_stats(self, mock_print, startup_info_display, mock_bot_stats) assert calls[1] == "Users: 150" assert calls[2] == "Channels: 45" - @patch('builtins.print') - @patch('src.bot.cogs.events.on_ready.bot_utils.get_current_date_time_str_long') + @patch("builtins.print") + @patch("src.bot.cogs.events.on_ready.bot_utils.get_current_date_time_str_long") def test_print_timestamp(self, mock_datetime, mock_print, startup_info_display): """Test printing timestamp.""" mock_datetime.return_value = "2023-01-01 12:00:00" @@ -146,7 +144,7 @@ async def test_setup_function(self, mock_bot): assert added_cog.bot == mock_bot @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_ready.bot_utils.get_bot_stats') + @patch("src.bot.cogs.events.on_ready.bot_utils.get_bot_stats") async def test_on_ready_event_success(self, mock_get_bot_stats, mock_bot, mock_bot_stats): """Test on_ready event handler success.""" mock_get_bot_stats.return_value = mock_bot_stats @@ -160,10 +158,8 @@ async def test_on_ready_event_success(self, mock_get_bot_stats, mock_bot, mock_b cog.info_display.print_bot_stats = MagicMock() cog.info_display.print_timestamp = MagicMock() - # Access the event handler directly - on_ready_event = mock_bot.event.call_args_list[0][0][0] - - await on_ready_event() + # Call the listener method directly + await cog.on_ready() # Verify all display methods were called cog.info_display.print_startup_banner.assert_called_once_with(variables.VERSION) @@ -176,36 +172,34 @@ async def test_on_ready_event_success(self, mock_get_bot_stats, mock_bot, mock_b mock_get_bot_stats.assert_called_once_with(mock_bot) # Verify log message - mock_bot.log.info.assert_called_once_with(messages.BOT_ONLINE.format(mock_bot.user)) + mock_bot.log.info.assert_called_once_with(messages.bot_online(mock_bot.user)) def test_on_ready_cog_inheritance(self, on_ready_cog): """Test that OnReady cog properly inherits from commands.Cog.""" from discord.ext import commands assert isinstance(on_ready_cog, commands.Cog) - assert hasattr(on_ready_cog, 'bot') + assert hasattr(on_ready_cog, "bot") @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_ready.bot_utils.get_bot_stats') + @patch("src.bot.cogs.events.on_ready.bot_utils.get_bot_stats") async def test_on_ready_event_with_different_stats(self, mock_get_bot_stats, mock_bot): """Test on_ready event with different bot statistics.""" - different_stats = {'servers': 10, 'users': 500, 'channels': 120} + different_stats = {"servers": 10, "users": 500, "channels": 120} mock_get_bot_stats.return_value = different_stats cog = OnReady(mock_bot) cog.info_display.print_bot_stats = MagicMock() - # Access the event handler directly - on_ready_event = mock_bot.event.call_args_list[0][0][0] - - await on_ready_event() + # Call the listener method directly + await cog.on_ready() # Verify stats were passed correctly cog.info_display.print_bot_stats.assert_called_once_with(different_stats) @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_ready.variables.VERSION', '3.0.0') - @patch('src.bot.cogs.events.on_ready.bot_utils.get_bot_stats') + @patch("src.bot.cogs.events.on_ready.variables.VERSION", "3.0.0") + @patch("src.bot.cogs.events.on_ready.bot_utils.get_bot_stats") async def test_on_ready_event_with_different_version(self, mock_get_bot_stats, mock_bot, mock_bot_stats): """Test on_ready event with different version.""" mock_get_bot_stats.return_value = mock_bot_stats @@ -213,13 +207,11 @@ async def test_on_ready_event_with_different_version(self, mock_get_bot_stats, m cog = OnReady(mock_bot) cog.info_display.print_startup_banner = MagicMock() - # Access the event handler directly - on_ready_event = mock_bot.event.call_args_list[0][0][0] - - await on_ready_event() + # Call the listener method directly + await cog.on_ready() # Verify version was passed correctly - cog.info_display.print_startup_banner.assert_called_once_with('3.0.0') + cog.info_display.print_startup_banner.assert_called_once_with("3.0.0") def test_startup_info_display_static_methods(self): """Test that all StartupInfoDisplay methods are static.""" @@ -231,7 +223,7 @@ def test_startup_info_display_static_methods(self): assert inspect.isfunction(StartupInfoDisplay.print_bot_stats) assert inspect.isfunction(StartupInfoDisplay.print_timestamp) - @patch('builtins.print') + @patch("builtins.print") def test_startup_info_display_integration(self, mock_print, mock_bot, mock_bot_stats): """Test integration of all StartupInfoDisplay methods.""" display = StartupInfoDisplay() @@ -246,10 +238,10 @@ def test_startup_info_display_integration(self, mock_print, mock_bot, mock_bot_s # Should have printed multiple lines assert mock_print.call_count >= 10 # At least 10 print calls - @patch('builtins.print') + @patch("builtins.print") def test_print_bot_stats_empty_stats(self, mock_print, startup_info_display): """Test printing bot statistics with empty stats.""" - empty_stats = {'servers': 0, 'users': 0, 'channels': 0} + empty_stats = {"servers": 0, "users": 0, "channels": 0} startup_info_display.print_bot_stats(empty_stats) @@ -258,7 +250,7 @@ def test_print_bot_stats_empty_stats(self, mock_print, startup_info_display): assert calls[1] == "Users: 0" assert calls[2] == "Channels: 0" - @patch('builtins.print') + @patch("builtins.print") def test_print_bot_info_complex_bot_name(self, mock_print, startup_info_display): """Test printing bot info with complex bot name.""" complex_bot = MagicMock() @@ -275,7 +267,7 @@ def test_print_bot_info_complex_bot_name(self, mock_print, startup_info_display) assert calls[2] == "Prefix: $$" @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_ready.bot_utils.get_bot_stats') + @patch("src.bot.cogs.events.on_ready.bot_utils.get_bot_stats") async def test_on_ready_event_bot_stats_error(self, mock_get_bot_stats, mock_bot): """Test on_ready event when bot stats retrieval fails.""" # This test verifies the event still completes even if bot_stats fails @@ -290,11 +282,9 @@ async def test_on_ready_event_bot_stats_error(self, mock_get_bot_stats, mock_bot cog.info_display.print_bot_stats = MagicMock() cog.info_display.print_timestamp = MagicMock() - # Access the event handler directly - on_ready_event = mock_bot.event.call_args_list[0][0][0] - + # Call the listener method directly # Should handle exception gracefully and not raise - await on_ready_event() + await cog.on_ready() # Startup banner should still have been called cog.info_display.print_startup_banner.assert_called_once() @@ -306,19 +296,17 @@ async def test_on_ready_event_bot_stats_error(self, mock_get_bot_stats, mock_bot cog.info_display.print_bot_stats.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.cogs.events.on_ready.messages.BOT_ONLINE', 'Bot {} is now online!') - @patch('src.bot.cogs.events.on_ready.bot_utils.get_bot_stats') - async def test_on_ready_event_custom_message(self, mock_get_bot_stats, mock_bot, mock_bot_stats): + @patch("src.bot.cogs.events.on_ready.messages.bot_online", return_value="Bot TestBot is now online!") + @patch("src.bot.cogs.events.on_ready.bot_utils.get_bot_stats") + async def test_on_ready_event_custom_message(self, mock_get_bot_stats, mock_bot_online, mock_bot, mock_bot_stats): """Test on_ready event with custom bot online message.""" mock_get_bot_stats.return_value = mock_bot_stats - OnReady(mock_bot) - - # Access the event handler directly - on_ready_event = mock_bot.event.call_args_list[0][0][0] + cog = OnReady(mock_bot) - await on_ready_event() + # Call the listener method directly + await cog.on_ready() # Verify custom log message format - expected_message = f'Bot {mock_bot.user} is now online!' - mock_bot.log.info.assert_called_once_with(expected_message) + mock_bot_online.assert_called_once_with(mock_bot.user) + mock_bot.log.info.assert_called_once_with("Bot TestBot is now online!") diff --git a/tests/unit/bot/test_discord_bot.py b/tests/unit/bot/test_discord_bot.py index 7263b7b1..94f0c5ef 100644 --- a/tests/unit/bot/test_discord_bot.py +++ b/tests/unit/bot/test_discord_bot.py @@ -3,7 +3,7 @@ import sys from unittest.mock import Mock -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() import discord import pytest @@ -15,10 +15,10 @@ class TestBotInit: """Test cases for Bot.__init__.""" - @patch('src.bot.discord_bot.profanity') - @patch('src.bot.discord_bot.get_gw2_settings') - @patch('src.bot.discord_bot.get_bot_settings') - @patch('src.bot.discord_bot.bot_utils') + @patch("src.bot.discord_bot.profanity") + @patch("src.bot.discord_bot.get_gw2_settings") + @patch("src.bot.discord_bot.get_bot_settings") + @patch("src.bot.discord_bot.bot_utils") def _create_bot(self, mock_bot_utils, mock_get_bot, mock_get_gw2, mock_profanity, **overrides): """Helper to create a Bot instance with all patches applied.""" mock_bot_utils.get_current_date_time.return_value = MagicMock() @@ -87,10 +87,10 @@ def test_bot_init_loads_profanity(self): mock_profanity.load_censor_words.assert_called_once() assert bot.profanity is mock_profanity - @patch('src.bot.discord_bot.profanity') - @patch('src.bot.discord_bot.get_gw2_settings') - @patch('src.bot.discord_bot.get_bot_settings') - @patch('src.bot.discord_bot.bot_utils') + @patch("src.bot.discord_bot.profanity") + @patch("src.bot.discord_bot.get_gw2_settings") + @patch("src.bot.discord_bot.get_bot_settings") + @patch("src.bot.discord_bot.bot_utils") def test_bot_init_calls_load_settings(self, mock_bot_utils, mock_get_bot, mock_get_gw2, mock_profanity): """Verify _load_settings is called during __init__.""" mock_bot_utils.get_current_date_time.return_value = MagicMock() @@ -102,7 +102,7 @@ def test_bot_init_calls_load_settings(self, mock_bot_utils, mock_get_bot, mock_g mock_gw2_settings = MagicMock() mock_get_gw2.return_value = mock_gw2_settings - with patch.object(Bot, '_load_settings') as mock_load: + with patch.object(Bot, "_load_settings") as mock_load: bot = Bot( command_prefix="!", intents=discord.Intents.default(), @@ -116,10 +116,10 @@ def test_bot_init_calls_load_settings(self, mock_bot_utils, mock_get_bot, mock_g class TestLoadSettings: """Test cases for Bot._load_settings.""" - @patch('src.bot.discord_bot.profanity') - @patch('src.bot.discord_bot.get_gw2_settings') - @patch('src.bot.discord_bot.get_bot_settings') - @patch('src.bot.discord_bot.bot_utils') + @patch("src.bot.discord_bot.profanity") + @patch("src.bot.discord_bot.get_gw2_settings") + @patch("src.bot.discord_bot.get_bot_settings") + @patch("src.bot.discord_bot.bot_utils") def test_load_settings_populates_bot_settings(self, mock_bot_utils, mock_get_bot, mock_get_gw2, mock_profanity): """Verify _load_settings populates self.settings['bot'] and self.settings['gw2'].""" mock_bot_utils.get_current_date_time.return_value = MagicMock() @@ -150,17 +150,17 @@ def test_load_settings_populates_bot_settings(self, mock_bot_utils, mock_get_bot ) assert bot.settings["bot"]["BGActivityTimer"] == 300 - assert bot.settings["bot"]["AllowedDMCommands"] == "owner, about" - assert bot.settings["bot"]["BotReactionWords"] == "stupid, retard" + assert bot.settings["bot"]["AllowedDMCommands"] == ["owner", "about"] + assert bot.settings["bot"]["BotReactionWords"] == ["stupid", "retard"] assert bot.settings["bot"]["EmbedColor"] == embed_color assert bot.settings["bot"]["EmbedOwnerColor"] == owner_color assert bot.settings["bot"]["ExclusiveUsers"] == "user1" assert bot.settings["gw2"]["EmbedColor"] == gw2_color - @patch('src.bot.discord_bot.profanity') - @patch('src.bot.discord_bot.get_gw2_settings') - @patch('src.bot.discord_bot.get_bot_settings') - @patch('src.bot.discord_bot.bot_utils') + @patch("src.bot.discord_bot.profanity") + @patch("src.bot.discord_bot.get_gw2_settings") + @patch("src.bot.discord_bot.get_bot_settings") + @patch("src.bot.discord_bot.bot_utils") def test_load_settings_error_raises(self, mock_bot_utils, mock_get_bot, mock_get_gw2, mock_profanity): """Verify exception propagates and log.error is called when _load_settings fails.""" mock_bot_utils.get_current_date_time.return_value = MagicMock() @@ -181,15 +181,131 @@ def test_load_settings_error_raises(self, mock_bot_utils, mock_get_bot, mock_get error_msg = log.error.call_args[0][0] assert messages.BOT_LOAD_SETTINGS_FAILED in error_msg + @patch("src.bot.discord_bot.profanity") + @patch("src.bot.discord_bot.get_gw2_settings") + @patch("src.bot.discord_bot.get_bot_settings") + @patch("src.bot.discord_bot.bot_utils") + def test_allowed_dm_commands_parsed_as_list(self, mock_bot_utils, mock_get_bot, mock_get_gw2, mock_profanity): + """Verify comma-separated allowed_dm_commands string is parsed into a list.""" + mock_bot_utils.get_current_date_time.return_value = MagicMock() + mock_bot_utils.get_color_settings.return_value = discord.Color.green() + + mock_bot_settings = MagicMock() + mock_bot_settings.allowed_dm_commands = "owner, about, gw2" + mock_bot_settings.bot_reaction_words = "" + mock_get_bot.return_value = mock_bot_settings + + mock_gw2_settings = MagicMock() + mock_get_gw2.return_value = mock_gw2_settings + + bot = Bot( + command_prefix="!", + intents=discord.Intents.default(), + aiosession=MagicMock(), + db_session=MagicMock(), + log=MagicMock(), + ) + + result = bot.settings["bot"]["AllowedDMCommands"] + assert isinstance(result, list) + assert result == ["owner", "about", "gw2"] + + @patch("src.bot.discord_bot.profanity") + @patch("src.bot.discord_bot.get_gw2_settings") + @patch("src.bot.discord_bot.get_bot_settings") + @patch("src.bot.discord_bot.bot_utils") + def test_allowed_dm_commands_empty_returns_none(self, mock_bot_utils, mock_get_bot, mock_get_gw2, mock_profanity): + """Verify empty allowed_dm_commands string returns None.""" + mock_bot_utils.get_current_date_time.return_value = MagicMock() + mock_bot_utils.get_color_settings.return_value = discord.Color.green() + + mock_bot_settings = MagicMock() + mock_bot_settings.allowed_dm_commands = "" + mock_bot_settings.bot_reaction_words = "" + mock_get_bot.return_value = mock_bot_settings + + mock_gw2_settings = MagicMock() + mock_get_gw2.return_value = mock_gw2_settings + + bot = Bot( + command_prefix="!", + intents=discord.Intents.default(), + aiosession=MagicMock(), + db_session=MagicMock(), + log=MagicMock(), + ) + + assert bot.settings["bot"]["AllowedDMCommands"] is None + + @patch("src.bot.discord_bot.profanity") + @patch("src.bot.discord_bot.get_gw2_settings") + @patch("src.bot.discord_bot.get_bot_settings") + @patch("src.bot.discord_bot.bot_utils") + def test_bot_reaction_words_parsed_as_list(self, mock_bot_utils, mock_get_bot, mock_get_gw2, mock_profanity): + """Verify comma-separated bot_reaction_words string is parsed into a list.""" + mock_bot_utils.get_current_date_time.return_value = MagicMock() + mock_bot_utils.get_color_settings.return_value = discord.Color.green() + + mock_bot_settings = MagicMock() + mock_bot_settings.allowed_dm_commands = "owner" + mock_bot_settings.bot_reaction_words = "stupid, retard, idiot" + mock_get_bot.return_value = mock_bot_settings + + mock_gw2_settings = MagicMock() + mock_get_gw2.return_value = mock_gw2_settings + + bot = Bot( + command_prefix="!", + intents=discord.Intents.default(), + aiosession=MagicMock(), + db_session=MagicMock(), + log=MagicMock(), + ) + + result = bot.settings["bot"]["BotReactionWords"] + assert isinstance(result, list) + assert result == ["stupid", "retard", "idiot"] + + @patch("src.bot.discord_bot.profanity") + @patch("src.bot.discord_bot.get_gw2_settings") + @patch("src.bot.discord_bot.get_bot_settings") + @patch("src.bot.discord_bot.bot_utils") + def test_bot_reaction_words_empty_returns_empty_list( + self, mock_bot_utils, mock_get_bot, mock_get_gw2, mock_profanity + ): + """Verify empty bot_reaction_words string returns empty list.""" + mock_bot_utils.get_current_date_time.return_value = MagicMock() + mock_bot_utils.get_color_settings.return_value = discord.Color.green() + + mock_bot_settings = MagicMock() + mock_bot_settings.allowed_dm_commands = "owner" + mock_bot_settings.bot_reaction_words = "" + mock_get_bot.return_value = mock_bot_settings + + mock_gw2_settings = MagicMock() + mock_get_gw2.return_value = mock_gw2_settings + + bot = Bot( + command_prefix="!", + intents=discord.Intents.default(), + aiosession=MagicMock(), + db_session=MagicMock(), + log=MagicMock(), + ) + + result = bot.settings["bot"]["BotReactionWords"] + assert isinstance(result, list) + assert result == [] + class TestSetupHook: """Test cases for Bot.setup_hook.""" @pytest.mark.asyncio - @patch('src.bot.discord_bot.profanity') - @patch('src.bot.discord_bot.get_gw2_settings') - @patch('src.bot.discord_bot.get_bot_settings') - @patch('src.bot.discord_bot.bot_utils') + @patch("src.bot.discord_bot.profanity") + @patch("src.bot.discord_bot.get_gw2_settings") + @patch("src.bot.discord_bot.get_bot_settings") + @patch("src.bot.discord_bot.bot_utils") async def test_setup_hook_loads_cogs(self, mock_bot_utils, mock_get_bot, mock_get_gw2, mock_profanity): """Verify setup_hook calls bot_utils.load_cogs and logs success.""" mock_bot_utils.get_current_date_time.return_value = MagicMock() @@ -217,10 +333,10 @@ async def test_setup_hook_loads_cogs(self, mock_bot_utils, mock_get_bot, mock_ge log.info.assert_any_call(messages.BOT_LOADED_ALL_COGS_SUCCESS) @pytest.mark.asyncio - @patch('src.bot.discord_bot.profanity') - @patch('src.bot.discord_bot.get_gw2_settings') - @patch('src.bot.discord_bot.get_bot_settings') - @patch('src.bot.discord_bot.bot_utils') + @patch("src.bot.discord_bot.profanity") + @patch("src.bot.discord_bot.get_gw2_settings") + @patch("src.bot.discord_bot.get_bot_settings") + @patch("src.bot.discord_bot.bot_utils") async def test_setup_hook_failure_logs_and_raises(self, mock_bot_utils, mock_get_bot, mock_get_gw2, mock_profanity): """Verify setup_hook logs error and re-raises when load_cogs fails.""" mock_bot_utils.get_current_date_time.return_value = MagicMock() diff --git a/tests/unit/bot/test_main.py b/tests/unit/bot/test_main.py index 7204117a..2bbdd966 100644 --- a/tests/unit/bot/test_main.py +++ b/tests/unit/bot/test_main.py @@ -3,7 +3,7 @@ import sys from unittest.mock import Mock -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() import discord import pytest @@ -21,7 +21,7 @@ async def test_get_command_prefix_success(self): mock_db_session = MagicMock() mock_log = MagicMock() - with patch('src.__main__.BotConfigsDal') as mock_dal_class: + with patch("src.__main__.BotConfigsDal") as mock_dal_class: mock_dal = AsyncMock() mock_dal.get_bot_prefix.return_value = "!!" mock_dal_class.return_value = mock_dal @@ -38,7 +38,7 @@ async def test_get_command_prefix_none_returns_default(self): mock_db_session = MagicMock() mock_log = MagicMock() - with patch('src.__main__.BotConfigsDal') as mock_dal_class: + with patch("src.__main__.BotConfigsDal") as mock_dal_class: mock_dal = AsyncMock() mock_dal.get_bot_prefix.return_value = None mock_dal_class.return_value = mock_dal @@ -53,7 +53,7 @@ async def test_get_command_prefix_exception_returns_default(self): mock_db_session = MagicMock() mock_log = MagicMock() - with patch('src.__main__.BotConfigsDal') as mock_dal_class: + with patch("src.__main__.BotConfigsDal") as mock_dal_class: mock_dal_class.side_effect = RuntimeError("db connection failed") result = await _get_command_prefix(mock_db_session, mock_log) @@ -67,14 +67,14 @@ async def test_get_command_prefix_exception_returns_default(self): class TestCreateBotActivity: """Test cases for _create_bot_activity.""" - @patch('src.__main__.get_bot_settings') + @patch("src.__main__.get_bot_settings") def test_create_bot_activity_no_exclusive_users(self, mock_get_bot_settings): """Verify a Game activity is returned with a game from GAMES_INCLUDED when no exclusive users.""" mock_settings = MagicMock() mock_settings.exclusive_users = "" mock_get_bot_settings.return_value = mock_settings - with patch('src.__main__.random.SystemRandom') as mock_sys_random_class: + with patch("src.__main__.random.SystemRandom") as mock_sys_random_class: mock_rng = MagicMock() mock_rng.choice.return_value = "Guild Wars 2" mock_sys_random_class.return_value = mock_rng @@ -86,7 +86,7 @@ def test_create_bot_activity_no_exclusive_users(self, mock_get_bot_settings): assert "!help" in result.name mock_rng.choice.assert_called_once_with(variables.GAMES_INCLUDED) - @patch('src.__main__.get_bot_settings') + @patch("src.__main__.get_bot_settings") def test_create_bot_activity_exclusive_users(self, mock_get_bot_settings): """Verify 'PRIVATE BOT' activity is returned when exclusive_users is non-empty.""" mock_settings = MagicMock() @@ -99,7 +99,7 @@ def test_create_bot_activity_exclusive_users(self, mock_get_bot_settings): assert "PRIVATE BOT" in result.name assert "!help" in result.name - @patch('src.__main__.get_bot_settings') + @patch("src.__main__.get_bot_settings") def test_create_bot_activity_custom_prefix(self, mock_get_bot_settings): """Verify the activity includes the custom prefix in the help command.""" mock_settings = MagicMock() @@ -115,11 +115,11 @@ def test_create_bot_activity_custom_prefix(self, mock_get_bot_settings): class TestRunBot: """Test cases for run_bot.""" - @patch('src.__main__.main', new_callable=MagicMock) - @patch('src.__main__.sys.exit') - @patch('src.__main__.print') - @patch('src.__main__.time.sleep') - @patch('src.__main__.asyncio.run') + @patch("src.__main__.main", new_callable=MagicMock) + @patch("src.__main__.sys.exit") + @patch("src.__main__.print") + @patch("src.__main__.time.sleep") + @patch("src.__main__.asyncio.run") def test_run_bot_keyboard_interrupt(self, mock_asyncio_run, mock_sleep, mock_print, mock_exit, mock_main): """Verify KeyboardInterrupt is caught and CTRLC message is printed.""" mock_asyncio_run.side_effect = KeyboardInterrupt @@ -133,11 +133,11 @@ def test_run_bot_keyboard_interrupt(self, mock_asyncio_run, mock_sleep, mock_pri assert any(messages.BOT_STOPPED_CTRTC == c for c in calls) mock_exit.assert_not_called() - @patch('src.__main__.main', new_callable=MagicMock) - @patch('src.__main__.sys.exit') - @patch('src.__main__.print') - @patch('src.__main__.time.sleep') - @patch('src.__main__.asyncio.run') + @patch("src.__main__.main", new_callable=MagicMock) + @patch("src.__main__.sys.exit") + @patch("src.__main__.print") + @patch("src.__main__.time.sleep") + @patch("src.__main__.asyncio.run") def test_run_bot_exception(self, mock_asyncio_run, mock_sleep, mock_print, mock_exit, mock_main): """Verify generic exceptions cause sys.exit(1).""" mock_asyncio_run.side_effect = RuntimeError("unexpected crash") @@ -150,11 +150,11 @@ def test_run_bot_exception(self, mock_asyncio_run, mock_sleep, mock_print, mock_ calls = [call[0][0] for call in mock_print.call_args_list] assert any(messages.BOT_CRASHED in c for c in calls) - @patch('src.__main__.main', new_callable=MagicMock) - @patch('src.__main__.sys.exit') - @patch('src.__main__.print') - @patch('src.__main__.time.sleep') - @patch('src.__main__.asyncio.run') + @patch("src.__main__.main", new_callable=MagicMock) + @patch("src.__main__.sys.exit") + @patch("src.__main__.print") + @patch("src.__main__.time.sleep") + @patch("src.__main__.asyncio.run") def test_run_bot_prints_starting_message(self, mock_asyncio_run, mock_sleep, mock_print, mock_exit, mock_main): """Verify run_bot prints the starting message and sleeps before running.""" mock_asyncio_run.return_value = None @@ -163,6 +163,6 @@ def test_run_bot_prints_starting_message(self, mock_asyncio_run, mock_sleep, moc # First print should be the starting message first_print = mock_print.call_args_list[0][0][0] - expected_start_msg = messages.BOT_STARTING.format(variables.TIME_BEFORE_START) + expected_start_msg = messages.bot_starting(variables.TIME_BEFORE_START) assert first_print == expected_start_msg mock_sleep.assert_called_once_with(variables.TIME_BEFORE_START) diff --git a/tests/unit/bot/tools/test_background_tasks.py b/tests/unit/bot/tools/test_background_tasks.py index 5a7271e8..1c2fc5db 100644 --- a/tests/unit/bot/tools/test_background_tasks.py +++ b/tests/unit/bot/tools/test_background_tasks.py @@ -25,7 +25,7 @@ def test_init(self, mock_bot): bg_tasks = BackGroundTasks(mock_bot) assert bg_tasks.bot is mock_bot - assert hasattr(bg_tasks, 'random') + assert hasattr(bg_tasks, "random") assert bg_tasks.random is not None @pytest.mark.asyncio @@ -44,7 +44,7 @@ def mock_is_closed(): bg_tasks = BackGroundTasks(mock_bot) # Mock asyncio.sleep to prevent actual waiting - with patch('asyncio.sleep', new_callable=AsyncMock) as mock_sleep: + with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep: await bg_tasks.change_presence_task(5) # Verify bot.wait_until_ready was called @@ -55,18 +55,18 @@ def mock_is_closed(): # Verify the call arguments call_args = mock_bot.change_presence.call_args - assert call_args[1]['status'] == discord.Status.online - assert isinstance(call_args[1]['activity'], discord.Game) + assert call_args[1]["status"] == discord.Status.online + assert isinstance(call_args[1]["activity"], discord.Game) # Verify activity description format - activity_name = call_args[1]['activity'].name - assert ' | !help' in activity_name + activity_name = call_args[1]["activity"].name + assert " | !help" in activity_name # Activity name should contain some game and the help command # Verify logging mock_bot.log.info.assert_called_once() log_message = mock_bot.log.info.call_args[0][0] - assert 'Background task (5s) - Changing activity:' in log_message + assert "Background task (5s) - Changing activity:" in log_message # Verify sleep was called with correct interval mock_sleep.assert_called_once_with(5) @@ -86,7 +86,7 @@ def mock_is_closed(): bg_tasks = BackGroundTasks(mock_bot) - with patch('asyncio.sleep', new_callable=AsyncMock) as mock_sleep: + with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep: await bg_tasks.change_presence_task(10) # Verify multiple calls @@ -113,18 +113,18 @@ def mock_is_closed(): bg_tasks = BackGroundTasks(mock_bot) - with patch('asyncio.sleep', new_callable=AsyncMock): + with patch("asyncio.sleep", new_callable=AsyncMock): await bg_tasks.change_presence_task(1) # Collect all activity names activity_names = [] for call in mock_bot.change_presence.call_args_list: - activity_name = call[1]['activity'].name + activity_name = call[1]["activity"].name activity_names.append(activity_name) # Verify all names contain the help command for name in activity_names: - assert ' | !help' in name + assert " | !help" in name @pytest.mark.asyncio async def test_change_presence_task_exception_handling(self, mock_bot): @@ -144,7 +144,7 @@ def mock_is_closed(): bg_tasks = BackGroundTasks(mock_bot) - with patch('asyncio.sleep', new_callable=AsyncMock) as mock_sleep: + with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep: await bg_tasks.change_presence_task(3) # Verify error was logged @@ -166,7 +166,7 @@ async def test_change_presence_task_wait_until_ready_exception(self, mock_bot): bg_tasks = BackGroundTasks(mock_bot) # Should raise exception since wait_until_ready fails - with patch('asyncio.sleep', new_callable=AsyncMock): + with patch("asyncio.sleep", new_callable=AsyncMock): with pytest.raises(Exception, match="Ready error"): await bg_tasks.change_presence_task(1) @@ -192,11 +192,11 @@ def mock_is_closed(): bg_tasks = BackGroundTasks(mock_bot) - with patch('asyncio.sleep', new_callable=AsyncMock): + with patch("asyncio.sleep", new_callable=AsyncMock): await bg_tasks.change_presence_task(1) # Verify activity contains the first prefix - activity_name = mock_bot.change_presence.call_args[1]['activity'].name + activity_name = mock_bot.change_presence.call_args[1]["activity"].name expected_suffix = f" | {prefixes[0]}help" assert activity_name.endswith(expected_suffix) @@ -220,7 +220,7 @@ def mock_is_closed(): # Mock the random choice to raise IndexError bg_tasks.random.choice = MagicMock(side_effect=IndexError("list index out of range")) - with patch('asyncio.sleep', new_callable=AsyncMock): + with patch("asyncio.sleep", new_callable=AsyncMock): # Should not raise IndexError, should be caught and logged as an error await bg_tasks.change_presence_task(1) @@ -247,7 +247,7 @@ def mock_is_closed(): bg_tasks = BackGroundTasks(mock_bot) - with patch('asyncio.sleep', new_callable=AsyncMock) as mock_sleep: + with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep: await bg_tasks.change_presence_task(2) # Verify errors were logged for each attempt @@ -289,8 +289,8 @@ def test_random_instance_creation(self): # Should have different random instances assert bg_tasks1.random is not bg_tasks2.random - assert type(bg_tasks1.random).__name__ == 'SystemRandom' - assert type(bg_tasks2.random).__name__ == 'SystemRandom' + assert type(bg_tasks1.random).__name__ == "SystemRandom" + assert type(bg_tasks2.random).__name__ == "SystemRandom" def test_random_choice_usage(self): """Test that SystemRandom.choice is used correctly.""" @@ -298,13 +298,13 @@ def test_random_choice_usage(self): bg_tasks = BackGroundTasks(mock_bot) # Mock the random choice method - bg_tasks.random.choice = MagicMock(return_value='SelectedGame') + bg_tasks.random.choice = MagicMock(return_value="SelectedGame") # Test the choice method directly with a test list - test_games = ['Game1', 'Game2', 'Game3'] + test_games = ["Game1", "Game2", "Game3"] result = bg_tasks.random.choice(test_games) - assert result == 'SelectedGame' + assert result == "SelectedGame" bg_tasks.random.choice.assert_called_once_with(test_games) @@ -335,7 +335,7 @@ def mock_is_closed(): bg_tasks = BackGroundTasks(mock_bot) - with patch('asyncio.sleep', new_callable=AsyncMock) as mock_sleep: + with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep: await bg_tasks.change_presence_task(5) # Comprehensive verification @@ -348,10 +348,10 @@ def mock_is_closed(): # Verify activity structure for call in mock_bot.change_presence.call_args_list: - activity = call[1]['activity'] + activity = call[1]["activity"] assert isinstance(activity, discord.Game) - assert ' | !help' in activity.name - assert call[1]['status'] == discord.Status.online + assert " | !help" in activity.name + assert call[1]["status"] == discord.Status.online # Verify logging messages for call in mock_bot.log.info.call_args_list: diff --git a/tests/unit/bot/tools/test_bot_utils.py b/tests/unit/bot/tools/test_bot_utils.py index f4e9dabf..c9721fd3 100644 --- a/tests/unit/bot/tools/test_bot_utils.py +++ b/tests/unit/bot/tools/test_bot_utils.py @@ -25,29 +25,29 @@ def test_colors_enum_completeness(self): """Test that all expected colors are defined.""" colors = bot_utils.Colors expected_colors = [ - 'black', - 'teal', - 'dark_teal', - 'green', - 'dark_green', - 'blue', - 'dark_blue', - 'purple', - 'dark_purple', - 'magenta', - 'dark_magenta', - 'gold', - 'dark_gold', - 'orange', - 'dark_orange', - 'red', - 'dark_red', - 'lighter_grey', - 'dark_grey', - 'light_grey', - 'darker_grey', - 'blurple', - 'greyple', + "black", + "teal", + "dark_teal", + "green", + "dark_green", + "blue", + "dark_blue", + "purple", + "dark_purple", + "magenta", + "dark_magenta", + "gold", + "dark_gold", + "orange", + "dark_orange", + "red", + "dark_red", + "lighter_grey", + "dark_grey", + "light_grey", + "darker_grey", + "blurple", + "greyple", ] for color_name in expected_colors: @@ -75,8 +75,8 @@ def mock_server(self): return guild @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.ServersDal') - @patch('src.gw2.tools.gw2_utils.insert_gw2_server_configs') + @patch("src.bot.tools.bot_utils.ServersDal") + @patch("src.gw2.tools.gw2_utils.insert_gw2_server_configs") async def test_insert_server_success(self, mock_gw2_insert, mock_servers_dal, mock_bot, mock_server): """Test successful server insertion.""" mock_dal = AsyncMock() @@ -94,7 +94,7 @@ async def test_insert_server_success(self, mock_gw2_insert, mock_servers_dal, mo # Verify GW2 configs were inserted mock_gw2_insert.assert_called_once_with(mock_bot, mock_server) - @patch('src.bot.tools.bot_utils.BackGroundTasks') + @patch("src.bot.tools.bot_utils.BackGroundTasks") def test_init_background_tasks_enabled(self, mock_bg_tasks_class, mock_bot): """Test background task initialization when enabled.""" mock_bot.settings = {"bot": {"BGActivityTimer": 30}} @@ -144,12 +144,12 @@ def mock_bot(self): async def test_load_cogs_success(self, mock_bot): """Test successful cog loading.""" with patch( - 'src.bot.tools.bot_utils.variables.ALL_COGS', ['src/bot/cogs/test_cog.py', 'src/bot/events/test_event.py'] + "src.bot.tools.bot_utils.variables.ALL_COGS", ["src/bot/cogs/test_cog.py", "src/bot/events/test_event.py"] ): await bot_utils.load_cogs(mock_bot) # Verify extensions were loaded - expected_extensions = ['src.bot.cogs.test_cog', 'src.bot.events.test_event'] + expected_extensions = ["src.bot.cogs.test_cog", "src.bot.events.test_event"] assert mock_bot.load_extension.call_count == 2 actual_calls = [call[0][0] for call in mock_bot.load_extension.call_args_list] @@ -162,8 +162,8 @@ async def test_load_cogs_success(self, mock_bot): async def test_load_cogs_with_failure(self, mock_bot): """Test cog loading with some failures.""" with patch( - 'src.bot.tools.bot_utils.variables.ALL_COGS', - ['src/bot/cogs/working_cog.py', 'src/bot/cogs/broken_cog.py'], + "src.bot.tools.bot_utils.variables.ALL_COGS", + ["src/bot/cogs/working_cog.py", "src/bot/cogs/broken_cog.py"], ): # Make second cog fail to load mock_bot.load_extension.side_effect = [None, Exception("Failed to load")] @@ -196,7 +196,7 @@ def mock_ctx(self): return ctx @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_help_msg') + @patch("src.bot.tools.bot_utils.send_help_msg") async def test_invoke_subcommand_with_subcommand(self, mock_send_help, mock_ctx): """Test invoke_subcommand when subcommand exists.""" mock_subcommand = MagicMock() @@ -210,7 +210,7 @@ async def test_invoke_subcommand_with_subcommand(self, mock_send_help, mock_ctx) mock_ctx.message.channel.typing.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_help_msg') + @patch("src.bot.tools.bot_utils.send_help_msg") async def test_invoke_subcommand_without_subcommand(self, mock_send_help, mock_ctx): """Test invoke_subcommand when no subcommand exists.""" mock_ctx.invoked_subcommand = None @@ -223,7 +223,7 @@ async def test_invoke_subcommand_without_subcommand(self, mock_send_help, mock_c mock_send_help.assert_called_once_with(mock_ctx, mock_ctx.command) @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_help_msg') + @patch("src.bot.tools.bot_utils.send_help_msg") async def test_invoke_subcommand_no_command(self, mock_send_help, mock_ctx): """Test invoke_subcommand when no command exists.""" mock_ctx.invoked_subcommand = None @@ -302,7 +302,7 @@ def mock_ctx(self): return ctx @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_embed') + @patch("src.bot.tools.bot_utils.send_embed") async def test_send_msg_basic(self, mock_send_embed, mock_ctx): """Test basic send_msg functionality.""" await bot_utils.send_msg(mock_ctx, "Test message") @@ -317,7 +317,7 @@ async def test_send_msg_basic(self, mock_send_embed, mock_ctx): assert dm_arg is False @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_embed') + @patch("src.bot.tools.bot_utils.send_embed") async def test_send_msg_dm(self, mock_send_embed, mock_ctx): """Test send_msg with DM enabled.""" await bot_utils.send_msg(mock_ctx, "DM message", dm=True) @@ -327,7 +327,7 @@ async def test_send_msg_dm(self, mock_send_embed, mock_ctx): assert dm_arg is True @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_embed') + @patch("src.bot.tools.bot_utils.send_embed") async def test_send_warning_msg(self, mock_send_embed, mock_ctx): """Test send_warning_msg functionality.""" await bot_utils.send_warning_msg(mock_ctx, "Warning message") @@ -337,7 +337,7 @@ async def test_send_warning_msg(self, mock_send_embed, mock_ctx): assert embed_arg.color == discord.Color.orange() @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_embed') + @patch("src.bot.tools.bot_utils.send_embed") async def test_send_info_msg(self, mock_send_embed, mock_ctx): """Test send_info_msg functionality.""" await bot_utils.send_info_msg(mock_ctx, "Info message") @@ -347,7 +347,7 @@ async def test_send_info_msg(self, mock_send_embed, mock_ctx): assert embed_arg.color == discord.Color.blue() @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_embed') + @patch("src.bot.tools.bot_utils.send_embed") async def test_send_error_msg(self, mock_send_embed, mock_ctx): """Test send_error_msg functionality.""" await bot_utils.send_error_msg(mock_ctx, "Error message") @@ -455,7 +455,7 @@ def test_is_private_message_guild(self): class TestDateTimeUtilities: """Test date and time utility functions.""" - @patch('src.bot.tools.bot_utils.datetime') + @patch("src.bot.tools.bot_utils.datetime") def test_get_current_date_time(self, mock_datetime): """Test get_current_date_time function.""" mock_now = MagicMock() @@ -466,8 +466,8 @@ def test_get_current_date_time(self, mock_datetime): mock_datetime.now.assert_called_once_with(UTC) assert result == mock_now - @patch('src.bot.tools.bot_utils.get_current_date_time') - @patch('src.bot.tools.bot_utils.convert_datetime_to_str_long') + @patch("src.bot.tools.bot_utils.get_current_date_time") + @patch("src.bot.tools.bot_utils.convert_datetime_to_str_long") def test_get_current_date_time_str_long(self, mock_convert, mock_get_current): """Test get_current_date_time_str_long function.""" mock_datetime = MagicMock() @@ -480,7 +480,7 @@ def test_get_current_date_time_str_long(self, mock_convert, mock_get_current): mock_convert.assert_called_once_with(mock_datetime) assert result == "formatted_date" - @patch('src.bot.tools.bot_utils.variables.DATE_TIME_FORMATTER_STR', '%Y-%m-%d %H:%M:%S') + @patch("src.bot.tools.bot_utils.variables.DATE_TIME_FORMATTER_STR", "%Y-%m-%d %H:%M:%S") def test_convert_datetime_to_str_long(self): """Test convert_datetime_to_str_long function.""" test_date = datetime(2023, 1, 1, 12, 30, 45, tzinfo=UTC) @@ -489,8 +489,8 @@ def test_convert_datetime_to_str_long(self): assert result == "2023-01-01 12:30:45" - @patch('src.bot.tools.bot_utils.variables.DATE_FORMATTER', '%Y-%m-%d') - @patch('src.bot.tools.bot_utils.variables.TIME_FORMATTER', '%H:%M:%S') + @patch("src.bot.tools.bot_utils.variables.DATE_FORMATTER", "%Y-%m-%d") + @patch("src.bot.tools.bot_utils.variables.TIME_FORMATTER", "%H:%M:%S") def test_convert_datetime_to_str_short(self): """Test convert_datetime_to_str_short function.""" test_date = datetime(2023, 1, 1, 12, 30, 45, tzinfo=UTC) @@ -499,8 +499,8 @@ def test_convert_datetime_to_str_short(self): assert result == "2023-01-01 12:30:45" - @patch('src.bot.tools.bot_utils.variables.DATE_FORMATTER', '%Y-%m-%d') - @patch('src.bot.tools.bot_utils.variables.TIME_FORMATTER', '%H:%M:%S') + @patch("src.bot.tools.bot_utils.variables.DATE_FORMATTER", "%Y-%m-%d") + @patch("src.bot.tools.bot_utils.variables.TIME_FORMATTER", "%H:%M:%S") def test_convert_str_to_datetime_short(self): """Test convert_str_to_datetime_short function.""" date_string = "2023-01-01 12:30:45" @@ -614,7 +614,7 @@ class TestSystemChannelUtilities: """Test system channel related utilities.""" @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.get_server_system_channel') + @patch("src.bot.tools.bot_utils.get_server_system_channel") async def test_send_msg_to_system_channel_success(self, mock_get_channel): """Test send_msg_to_system_channel with successful send.""" mock_log = MagicMock() @@ -630,7 +630,7 @@ async def test_send_msg_to_system_channel_success(self, mock_get_channel): mock_log.error.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.get_server_system_channel') + @patch("src.bot.tools.bot_utils.get_server_system_channel") async def test_send_msg_to_system_channel_no_channel(self, mock_get_channel): """Test send_msg_to_system_channel when no channel found.""" mock_log = MagicMock() @@ -644,7 +644,7 @@ async def test_send_msg_to_system_channel_no_channel(self, mock_get_channel): mock_log.error.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.get_server_system_channel') + @patch("src.bot.tools.bot_utils.get_server_system_channel") async def test_send_msg_to_system_channel_with_fallback(self, mock_get_channel): """Test send_msg_to_system_channel with fallback to plain message.""" mock_log = MagicMock() @@ -694,11 +694,11 @@ def test_get_color_settings_invalid_color(self): assert result is None - @patch('random.SystemRandom') + @patch("random.SystemRandom") def test_get_color_settings_random_consistency(self, mock_system_random): """Test that random color generation is consistent.""" mock_random = MagicMock() - mock_random.choice.side_effect = ['A', 'B', 'C', 'D', 'E', 'F'] + mock_random.choice.side_effect = ["A", "B", "C", "D", "E", "F"] mock_system_random.return_value = mock_random result = bot_utils.get_color_settings("random") @@ -753,7 +753,7 @@ def test_get_bot_stats_no_start_time(self): bot.guilds = [] bot.start_time = None - with patch('src.bot.tools.bot_utils.get_current_date_time') as mock_get_time: + with patch("src.bot.tools.bot_utils.get_current_date_time") as mock_get_time: mock_current_time = MagicMock() mock_get_time.return_value = mock_current_time diff --git a/tests/unit/bot/tools/test_bot_utils_extra.py b/tests/unit/bot/tools/test_bot_utils_extra.py index c83b9edd..1deaa7c3 100644 --- a/tests/unit/bot/tools/test_bot_utils_extra.py +++ b/tests/unit/bot/tools/test_bot_utils_extra.py @@ -5,7 +5,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.bot.tools import bot_utils @@ -153,7 +153,7 @@ async def test_send_embed_dm_true(self, mock_ctx): # Should send notification embed to channel mock_ctx.send.assert_called_once() notification_call = mock_ctx.send.call_args - notification_embed = notification_call[1]['embed'] + notification_embed = notification_call[1]["embed"] assert isinstance(notification_embed, discord.Embed) assert notification_embed.color == discord.Color.green() assert "Response sent to your DM" in notification_embed.description @@ -166,7 +166,7 @@ async def test_send_embed_dm_true_notification_author_with_avatar(self, mock_ctx await bot_utils.send_embed(mock_ctx, embed, dm=True) notification_call = mock_ctx.send.call_args - notification_embed = notification_call[1]['embed'] + notification_embed = notification_call[1]["embed"] assert notification_embed.author.name == "TestUser" assert notification_embed.author.icon_url == "https://example.com/avatar.png" @@ -179,7 +179,7 @@ async def test_send_embed_dm_true_notification_author_no_avatar(self, mock_ctx): await bot_utils.send_embed(mock_ctx, embed, dm=True) notification_call = mock_ctx.send.call_args - notification_embed = notification_call[1]['embed'] + notification_embed = notification_call[1]["embed"] assert notification_embed.author.icon_url == "https://example.com/default.png" @pytest.mark.asyncio @@ -193,7 +193,7 @@ async def test_send_embed_normal_channel_send(self, mock_ctx): mock_ctx.author.send.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_error_msg') + @patch("src.bot.tools.bot_utils.send_error_msg") async def test_send_embed_discord_forbidden_exception(self, mock_send_error, mock_ctx): """Test send_embed handles discord.Forbidden exception.""" embed = discord.Embed(description="Test", color=discord.Color.green()) @@ -207,7 +207,7 @@ async def test_send_embed_discord_forbidden_exception(self, mock_send_error, moc assert call_args[0] == mock_ctx @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_error_msg') + @patch("src.bot.tools.bot_utils.send_error_msg") async def test_send_embed_discord_http_exception(self, mock_send_error, mock_ctx): """Test send_embed handles discord.HTTPException.""" embed = discord.Embed(description="Test", color=discord.Color.green()) @@ -252,7 +252,7 @@ def mock_ctx(self): return ctx @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_msg') + @patch("src.bot.tools.bot_utils.send_msg") async def test_delete_message_warning_true_success(self, mock_send_msg, mock_ctx): """Test delete_message with warning=True sends privacy message after successful delete.""" await bot_utils.delete_message(mock_ctx, warning=True) @@ -269,7 +269,7 @@ async def test_delete_message_warning_true_success(self, mock_send_msg, mock_ctx assert call_args[3] is None # color=None (no error) @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_msg') + @patch("src.bot.tools.bot_utils.send_msg") async def test_delete_message_warning_true_delete_fails(self, mock_send_msg, mock_ctx): """Test delete_message with warning=True sends error message when delete fails.""" mock_ctx.message.delete.side_effect = discord.Forbidden(MagicMock(), "No permission") @@ -286,7 +286,7 @@ async def test_delete_message_warning_true_delete_fails(self, mock_send_msg, moc assert call_args[3] == discord.Color.red() # color=red for error @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_msg') + @patch("src.bot.tools.bot_utils.send_msg") async def test_delete_message_warning_false_success(self, mock_send_msg, mock_ctx): """Test delete_message with warning=False does not send message after successful delete.""" await bot_utils.delete_message(mock_ctx, warning=False) @@ -295,7 +295,7 @@ async def test_delete_message_warning_false_success(self, mock_send_msg, mock_ct mock_send_msg.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_msg') + @patch("src.bot.tools.bot_utils.send_msg") async def test_delete_message_warning_false_delete_fails(self, mock_send_msg, mock_ctx): """Test delete_message with warning=False does not send message even when delete fails.""" mock_ctx.message.delete.side_effect = Exception("Delete failed") @@ -306,7 +306,7 @@ async def test_delete_message_warning_false_delete_fails(self, mock_send_msg, mo mock_ctx.bot.log.error.assert_called_once() @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_msg') + @patch("src.bot.tools.bot_utils.send_msg") async def test_delete_message_private_message_no_action(self, mock_send_msg): """Test delete_message does nothing for private messages.""" ctx = MagicMock() @@ -320,7 +320,7 @@ async def test_delete_message_private_message_no_action(self, mock_send_msg): mock_send_msg.assert_not_called() @pytest.mark.asyncio - @patch('src.bot.tools.bot_utils.send_msg') + @patch("src.bot.tools.bot_utils.send_msg") async def test_delete_message_warning_true_logs_error_on_failure(self, mock_send_msg, mock_ctx): """Test delete_message logs error when delete fails.""" mock_ctx.message.delete.side_effect = Exception("Permission denied") diff --git a/tests/unit/bot/tools/test_checks.py b/tests/unit/bot/tools/test_checks.py index b6ca5459..945f458e 100644 --- a/tests/unit/bot/tools/test_checks.py +++ b/tests/unit/bot/tools/test_checks.py @@ -70,10 +70,10 @@ def dummy_command(ctx): return "success" # Verify the decorator added command checks - assert hasattr(dummy_command, '__commands_checks__') + assert hasattr(dummy_command, "__commands_checks__") assert len(dummy_command.__commands_checks__) > 0 - @patch('src.bot.tools.checks.bot_utils.is_member_admin') + @patch("src.bot.tools.checks.bot_utils.is_member_admin") def test_check_is_admin_predicate_failure(self, mock_is_admin): """Test the predicate function when a user is not admin.""" mock_is_admin.return_value = False @@ -82,7 +82,7 @@ def test_check_is_admin_predicate_failure(self, mock_is_admin): # Extract the predicate function from the decorator # commands.check stores the predicate in the decorator's first argument - if hasattr(decorator, '__wrapped__'): + if hasattr(decorator, "__wrapped__"): predicate = decorator.__wrapped__ else: # Get the predicate from the closure @@ -102,8 +102,8 @@ def test_check_is_admin_predicate_failure(self, mock_is_admin): # If we can't access the predicate, just verify the decorator exists assert callable(decorator) - @patch('src.bot.tools.checks.commands.check') - @patch('src.bot.tools.checks.bot_utils.is_member_admin') + @patch("src.bot.tools.checks.commands.check") + @patch("src.bot.tools.checks.bot_utils.is_member_admin") def test_check_is_admin_uses_commands_check(self, mock_is_admin, mock_commands_check): """Test that check_is_admin uses commands.check.""" mock_commands_check.return_value = lambda f: f # Return identity function @@ -125,7 +125,7 @@ def test_command(ctx): return "command executed" # Verify the decorator was applied - assert hasattr(test_command, '__commands_checks__') + assert hasattr(test_command, "__commands_checks__") assert len(test_command.__commands_checks__) > 0 # The check should be a function @@ -154,10 +154,10 @@ def dummy_command(ctx): return "success" # Verify the decorator added command checks - assert hasattr(dummy_command, '__commands_checks__') + assert hasattr(dummy_command, "__commands_checks__") assert len(dummy_command.__commands_checks__) > 0 - @patch('src.bot.tools.checks.bot_utils.is_bot_owner') + @patch("src.bot.tools.checks.bot_utils.is_bot_owner") def test_check_is_bot_owner_predicate_failure(self, mock_is_owner): """Test the predicate function when user is not bot owner.""" mock_is_owner.return_value = False @@ -165,7 +165,7 @@ def test_check_is_bot_owner_predicate_failure(self, mock_is_owner): decorator = Checks.check_is_bot_owner() # Extract the predicate function from the decorator - if hasattr(decorator, '__wrapped__'): + if hasattr(decorator, "__wrapped__"): predicate = decorator.__wrapped__ else: # Get the predicate from the closure @@ -185,8 +185,8 @@ def test_check_is_bot_owner_predicate_failure(self, mock_is_owner): # If we can't access the predicate, just verify the decorator exists assert callable(decorator) - @patch('src.bot.tools.checks.commands.check') - @patch('src.bot.tools.checks.bot_utils.is_bot_owner') + @patch("src.bot.tools.checks.commands.check") + @patch("src.bot.tools.checks.bot_utils.is_bot_owner") def test_check_is_bot_owner_uses_commands_check(self, mock_is_owner, mock_commands_check): """Test that check_is_bot_owner uses commands.check.""" mock_commands_check.return_value = lambda f: f # Return identity function @@ -208,7 +208,7 @@ def test_command(ctx): return "command executed" # Verify the decorator was applied - assert hasattr(test_command, '__commands_checks__') + assert hasattr(test_command, "__commands_checks__") assert len(test_command.__commands_checks__) > 0 # The check should be a function @@ -237,7 +237,7 @@ def test_command(ctx): return "command executed" # Should have both checks - assert hasattr(test_command, '__commands_checks__') + assert hasattr(test_command, "__commands_checks__") assert len(test_command.__commands_checks__) == 2 def test_combined_decorators_both_pass(self): @@ -249,7 +249,7 @@ def test_command(ctx): return "success" # Should have both checks - assert hasattr(test_command, '__commands_checks__') + assert hasattr(test_command, "__commands_checks__") assert len(test_command.__commands_checks__) == 2 def test_combined_decorators_admin_fails(self): @@ -261,7 +261,7 @@ def test_command(ctx): return "command" # Should have both checks - assert hasattr(test_command, '__commands_checks__') + assert hasattr(test_command, "__commands_checks__") assert len(test_command.__commands_checks__) == 2 def test_combined_decorators_owner_fails(self): @@ -273,7 +273,7 @@ def test_command(ctx): return "command" # Should have both checks - assert hasattr(test_command, '__commands_checks__') + assert hasattr(test_command, "__commands_checks__") assert len(test_command.__commands_checks__) == 2 @@ -320,7 +320,7 @@ def test_command(ctx): return "success" # Verify decorator was applied - assert hasattr(test_command, '__commands_checks__') + assert hasattr(test_command, "__commands_checks__") def test_owner_check_with_none_context(self): """Test owner check decorator can be created.""" @@ -332,7 +332,7 @@ def test_command(ctx): return "success" # Verify decorator was applied - assert hasattr(test_command, '__commands_checks__') + assert hasattr(test_command, "__commands_checks__") def test_decorator_preserves_function_metadata(self): """Test that decorators preserve original function metadata.""" @@ -346,7 +346,7 @@ def test_command(ctx): assert "test_command" in test_command.__name__ or "wrapper" in test_command.__name__ # Should have check attributes - assert hasattr(test_command, '__commands_checks__') + assert hasattr(test_command, "__commands_checks__") def test_multiple_applications_of_same_decorator(self): """Test applying the same decorator multiple times.""" @@ -358,7 +358,7 @@ def test_command(ctx): return "success" # Should have multiple identical checks - assert hasattr(test_command, '__commands_checks__') + assert hasattr(test_command, "__commands_checks__") assert len(test_command.__commands_checks__) == 2 @@ -410,7 +410,7 @@ def test_static_method_accessibility(self): class TestCheckPredicateRaisesCheckFailure: """Tests that directly invoke the predicate to cover the raise CheckFailure lines (23 and 41).""" - @patch('src.bot.tools.checks.bot_utils.is_member_admin', return_value=False) + @patch("src.bot.tools.checks.bot_utils.is_member_admin", return_value=False) def test_admin_predicate_raises_check_failure(self, mock_is_admin): """Test that admin predicate raises CheckFailure when user is not admin (line 23).""" @@ -430,7 +430,7 @@ async def dummy(ctx): assert "User is not an administrator" in str(exc_info.value) mock_is_admin.assert_called_once_with(ctx.message.author) - @patch('src.bot.tools.checks.bot_utils.is_member_admin', return_value=True) + @patch("src.bot.tools.checks.bot_utils.is_member_admin", return_value=True) def test_admin_predicate_returns_true_when_admin(self, mock_is_admin): """Test that admin predicate returns True when user is admin (line 23).""" @@ -447,7 +447,7 @@ async def dummy(ctx): assert result is True mock_is_admin.assert_called_once_with(ctx.message.author) - @patch('src.bot.tools.checks.bot_utils.is_bot_owner', return_value=False) + @patch("src.bot.tools.checks.bot_utils.is_bot_owner", return_value=False) def test_owner_predicate_raises_check_failure(self, mock_is_owner): """Test that owner predicate raises CheckFailure when user is not owner (line 41).""" @@ -466,7 +466,7 @@ async def dummy(ctx): assert "User is not the bot owner" in str(exc_info.value) mock_is_owner.assert_called_once_with(ctx, ctx.message.author) - @patch('src.bot.tools.checks.bot_utils.is_bot_owner', return_value=True) + @patch("src.bot.tools.checks.bot_utils.is_bot_owner", return_value=True) def test_owner_predicate_returns_true_when_owner(self, mock_is_owner): """Test that owner predicate returns True when user is bot owner (line 41).""" diff --git a/tests/unit/bot/tools/test_cooldowns.py b/tests/unit/bot/tools/test_cooldowns.py index 113fc6bf..32f17f73 100644 --- a/tests/unit/bot/tools/test_cooldowns.py +++ b/tests/unit/bot/tools/test_cooldowns.py @@ -9,13 +9,13 @@ class TestCoolDownsEnum: def test_cooldowns_enum_exists(self): """Test that CoolDowns enum is properly defined.""" - assert hasattr(CoolDowns, 'Admin') - assert hasattr(CoolDowns, 'Config') - assert hasattr(CoolDowns, 'CustomCommand') - assert hasattr(CoolDowns, 'DiceRolls') - assert hasattr(CoolDowns, 'Misc') - assert hasattr(CoolDowns, 'OpenAI') - assert hasattr(CoolDowns, 'Owner') + assert hasattr(CoolDowns, "Admin") + assert hasattr(CoolDowns, "Config") + assert hasattr(CoolDowns, "CustomCommand") + assert hasattr(CoolDowns, "DiceRolls") + assert hasattr(CoolDowns, "Misc") + assert hasattr(CoolDowns, "OpenAI") + assert hasattr(CoolDowns, "Owner") def test_cooldowns_enum_values_are_integers(self): """Test that all cooldown values are integers.""" @@ -29,7 +29,7 @@ def test_cooldowns_enum_members_count(self): assert len(actual_members) >= 1 # At least Admin should exist # Check that Admin exists (which we know works) - assert 'Admin' in actual_members + assert "Admin" in actual_members class TestCoolDownsDebugMode: @@ -112,7 +112,7 @@ def test_configuration_file_loading(self): """Test that configuration loading works.""" # Since the configuration is loaded at import time, we can't mock it easily # Just verify that the CoolDowns enum was successfully created - assert hasattr(CoolDowns, 'Admin') + assert hasattr(CoolDowns, "Admin") assert isinstance(CoolDowns.Admin.value, int) def test_missing_configuration_values(self): @@ -171,7 +171,7 @@ def test_cooldowns_iteration(self): # Should have at least Admin assert len(cooldown_names) >= 1 - assert 'Admin' in cooldown_names + assert "Admin" in cooldown_names # All values should be positive integers for value in cooldown_values: @@ -180,10 +180,10 @@ def test_cooldowns_iteration(self): def test_cooldowns_lookup_by_name(self): """Test looking up cooldowns by name.""" - admin = CoolDowns['Admin'] + admin = CoolDowns["Admin"] assert admin == CoolDowns.Admin - config = CoolDowns['Config'] + config = CoolDowns["Config"] assert config == CoolDowns.Config # Test all cooldowns can be looked up @@ -269,7 +269,7 @@ class TestCoolDownsErrorHandling: def test_nonexistent_cooldown_access(self): """Test accessing non-existent cooldown raises appropriate error.""" with pytest.raises(KeyError): - _ = CoolDowns['NonExistentCooldown'] + _ = CoolDowns["NonExistentCooldown"] def test_cooldown_attribute_error(self): """Test that accessing non-existent attributes raises appropriate error.""" diff --git a/tests/unit/bot/tools/test_custom_help.py b/tests/unit/bot/tools/test_custom_help.py index 8234fbde..08dfd974 100644 --- a/tests/unit/bot/tools/test_custom_help.py +++ b/tests/unit/bot/tools/test_custom_help.py @@ -1,4 +1,4 @@ -"""Comprehensive tests for CustomHelpCommand class.""" +"""Comprehensive tests for CustomHelpCommand and HelpPaginatorView classes.""" import discord import pytest @@ -6,9 +6,193 @@ from discord.ext import commands from unittest.mock import AsyncMock, MagicMock, Mock -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() -from src.bot.tools.custom_help import CustomHelpCommand +from src.bot.tools.custom_help import CustomHelpCommand, HelpPaginatorView + + +class TestHelpPaginatorView: + """Test HelpPaginatorView pagination logic.""" + + @pytest.mark.asyncio + async def test_initial_state_first_page(self): + """Test view starts on page 0 with previous disabled and next enabled.""" + view = HelpPaginatorView(["Page A", "Page B", "Page C"], author_id=123) + + assert view.current_page == 0 + assert view.previous_button.disabled is True + assert view.next_button.disabled is False + assert view.page_indicator.label == "1/3" + assert view.page_indicator.disabled is True + + @pytest.mark.asyncio + async def test_initial_state_two_pages(self): + """Test view with two pages has correct initial state.""" + view = HelpPaginatorView(["Page A", "Page B"], author_id=456) + + assert view.current_page == 0 + assert view.previous_button.disabled is True + assert view.next_button.disabled is False + assert view.page_indicator.label == "1/2" + + @pytest.mark.asyncio + async def test_format_page_first(self): + """Test _format_page returns page header + content for first page.""" + view = HelpPaginatorView(["```\nContent A\n```", "```\nContent B\n```"], author_id=1) + + result = view._format_page() + + assert result == "**Page 1/2**\n```\nContent A\n```" + + @pytest.mark.asyncio + async def test_format_page_second(self): + """Test _format_page returns correct content after navigating.""" + view = HelpPaginatorView(["Page A", "Page B", "Page C"], author_id=1) + view.current_page = 1 + view._update_buttons() + + result = view._format_page() + + assert result == "**Page 2/3**\nPage B" + + @pytest.mark.asyncio + async def test_update_buttons_middle_page(self): + """Test buttons state on middle page: both enabled.""" + view = HelpPaginatorView(["A", "B", "C"], author_id=1) + view.current_page = 1 + view._update_buttons() + + assert view.previous_button.disabled is False + assert view.next_button.disabled is False + assert view.page_indicator.label == "2/3" + + @pytest.mark.asyncio + async def test_update_buttons_last_page(self): + """Test buttons state on last page: next disabled.""" + view = HelpPaginatorView(["A", "B", "C"], author_id=1) + view.current_page = 2 + view._update_buttons() + + assert view.previous_button.disabled is False + assert view.next_button.disabled is True + assert view.page_indicator.label == "3/3" + + @pytest.mark.asyncio + async def test_next_button_advances_page(self): + """Test clicking next button advances current_page.""" + view = HelpPaginatorView(["A", "B", "C"], author_id=42) + interaction = MagicMock() + interaction.user.id = 42 + interaction.response = AsyncMock() + + await view.next_button.callback(interaction) + + assert view.current_page == 1 + interaction.response.edit_message.assert_called_once() + call_kwargs = interaction.response.edit_message.call_args[1] + assert "**Page 2/3**" in call_kwargs["content"] + + @pytest.mark.asyncio + async def test_previous_button_goes_back(self): + """Test clicking previous button goes back a page.""" + view = HelpPaginatorView(["A", "B", "C"], author_id=42) + view.current_page = 2 + view._update_buttons() + interaction = MagicMock() + interaction.user.id = 42 + interaction.response = AsyncMock() + + await view.previous_button.callback(interaction) + + assert view.current_page == 1 + interaction.response.edit_message.assert_called_once() + call_kwargs = interaction.response.edit_message.call_args[1] + assert "**Page 2/3**" in call_kwargs["content"] + + @pytest.mark.asyncio + async def test_next_button_rejects_non_author(self): + """Test non-author clicking next gets ephemeral rejection.""" + view = HelpPaginatorView(["A", "B"], author_id=42) + interaction = MagicMock() + interaction.user.id = 999 + interaction.response = AsyncMock() + + await view.next_button.callback(interaction) + + assert view.current_page == 0 # unchanged + interaction.response.send_message.assert_called_once_with( + "Only the command invoker can use these buttons.", ephemeral=True + ) + + @pytest.mark.asyncio + async def test_previous_button_rejects_non_author(self): + """Test non-author clicking previous gets ephemeral rejection.""" + view = HelpPaginatorView(["A", "B"], author_id=42) + view.current_page = 1 + view._update_buttons() + interaction = MagicMock() + interaction.user.id = 999 + interaction.response = AsyncMock() + + await view.previous_button.callback(interaction) + + assert view.current_page == 1 # unchanged + interaction.response.send_message.assert_called_once_with( + "Only the command invoker can use these buttons.", ephemeral=True + ) + + @pytest.mark.asyncio + async def test_page_indicator_defers(self): + """Test page indicator button just defers (non-interactive).""" + view = HelpPaginatorView(["A", "B"], author_id=1) + interaction = MagicMock() + interaction.response = AsyncMock() + + await view.page_indicator.callback(interaction) + + interaction.response.defer.assert_called_once() + + @pytest.mark.asyncio + async def test_on_timeout_disables_all_buttons(self): + """Test on_timeout disables all children and edits message.""" + view = HelpPaginatorView(["A", "B"], author_id=1) + view.message = AsyncMock() + + await view.on_timeout() + + for item in view.children: + assert item.disabled is True + view.message.edit.assert_called_once_with(view=view) + + @pytest.mark.asyncio + async def test_on_timeout_no_message(self): + """Test on_timeout with no message reference does not raise.""" + view = HelpPaginatorView(["A", "B"], author_id=1) + view.message = None + + await view.on_timeout() + + for item in view.children: + assert item.disabled is True + + @pytest.mark.asyncio + async def test_timeout_is_300(self): + """Test view timeout is 300 seconds.""" + view = HelpPaginatorView(["A", "B"], author_id=1) + assert view.timeout == 300 + + @pytest.mark.asyncio + async def test_author_id_stored(self): + """Test author_id is stored correctly.""" + view = HelpPaginatorView(["A"], author_id=12345) + assert view.author_id == 12345 + + @pytest.mark.asyncio + async def test_pages_stored(self): + """Test pages list is stored correctly.""" + pages = ["Page 1", "Page 2"] + view = HelpPaginatorView(pages, author_id=1) + assert view.pages is pages class TestCustomHelpCommandInit: @@ -20,25 +204,25 @@ def test_init_default_options(self): assert cmd.paginator is not None assert isinstance(cmd.paginator, commands.Paginator) - assert cmd.paginator.prefix == '```' - assert cmd.paginator.suffix == '```' + assert cmd.paginator.prefix == "```" + assert cmd.paginator.suffix == "```" assert cmd.paginator.max_size == 2000 def test_init_custom_paginator(self): """Test initialization with custom paginator preserves it.""" - custom_paginator = commands.Paginator(prefix='---', suffix='---', max_size=1000) + custom_paginator = commands.Paginator(prefix="---", suffix="---", max_size=1000) cmd = CustomHelpCommand(paginator=custom_paginator) assert cmd.paginator is custom_paginator - assert cmd.paginator.prefix == '---' - assert cmd.paginator.suffix == '---' + assert cmd.paginator.prefix == "---" + assert cmd.paginator.suffix == "---" assert cmd.paginator.max_size == 1000 def test_init_custom_options_passed_to_parent(self): """Test that other options are passed to the parent class.""" - cmd = CustomHelpCommand(no_category='Uncategorized', sort_commands=False) + cmd = CustomHelpCommand(no_category="Uncategorized", sort_commands=False) - assert cmd.no_category == 'Uncategorized' + assert cmd.no_category == "Uncategorized" assert cmd.sort_commands is False def test_init_dm_help_option(self): @@ -99,7 +283,7 @@ async def test_send_pages_dm_help_true_not_in_dm(self, help_command): # Should send notification embed to channel help_command.context.send.assert_called_once() call_kwargs = help_command.context.send.call_args[1] - embed = call_kwargs['embed'] + embed = call_kwargs["embed"] assert isinstance(embed, discord.Embed) assert embed.color == discord.Color.green() @@ -117,7 +301,7 @@ async def test_send_pages_dm_help_true_notification_embed_description(self, help await help_command.send_pages() call_kwargs = help_command.context.send.call_args[1] - embed = call_kwargs['embed'] + embed = call_kwargs["embed"] assert "Response sent to your DM" in embed.description @pytest.mark.asyncio @@ -137,7 +321,7 @@ async def test_send_pages_dm_help_true_author_with_avatar(self, help_command): await help_command.send_pages() call_kwargs = help_command.context.send.call_args[1] - embed = call_kwargs['embed'] + embed = call_kwargs["embed"] assert embed.author.name == "TestUser" assert embed.author.icon_url == "https://cdn.example.com/avatar.png" @@ -160,7 +344,7 @@ async def test_send_pages_dm_help_true_author_no_avatar(self, help_command): await help_command.send_pages() call_kwargs = help_command.context.send.call_args[1] - embed = call_kwargs['embed'] + embed = call_kwargs["embed"] assert embed.author.icon_url == "https://cdn.example.com/default.png" @pytest.mark.asyncio @@ -205,12 +389,13 @@ def help_command(self): cmd = CustomHelpCommand() cmd.context = MagicMock() cmd.context.author = MagicMock() + cmd.context.author.id = 42 cmd.context.author.send = AsyncMock() return cmd @pytest.mark.asyncio async def test_send_pages_to_dm_single_page(self, help_command): - """Test _send_pages_to_dm with a single page does not add page header.""" + """Test _send_pages_to_dm with a single page does not add view.""" help_command.paginator = MagicMock() help_command.paginator.pages = ["```\nHelp content here\n```"] @@ -219,40 +404,46 @@ async def test_send_pages_to_dm_single_page(self, help_command): help_command.context.author.send.assert_called_once_with("```\nHelp content here\n```") @pytest.mark.asyncio - async def test_send_pages_to_dm_multiple_pages(self, help_command): - """Test _send_pages_to_dm with multiple pages adds page headers.""" + async def test_send_pages_to_dm_multiple_pages_sends_single_message(self, help_command): + """Test _send_pages_to_dm with multiple pages sends one message with view.""" help_command.paginator = MagicMock() help_command.paginator.pages = ["```\nPage 1 content\n```", "```\nPage 2 content\n```"] await help_command._send_pages_to_dm() - assert help_command.context.author.send.call_count == 2 - - first_call_content = help_command.context.author.send.call_args_list[0][0][0] - assert first_call_content == "**Page 1/2**\n```\nPage 1 content\n```" - - second_call_content = help_command.context.author.send.call_args_list[1][0][0] - assert second_call_content == "**Page 2/2**\n```\nPage 2 content\n```" + # Should send exactly one message (not two) + help_command.context.author.send.assert_called_once() + call_kwargs = help_command.context.author.send.call_args[1] + assert "**Page 1/2**" in call_kwargs["content"] + assert isinstance(call_kwargs["view"], HelpPaginatorView) @pytest.mark.asyncio - async def test_send_pages_to_dm_three_pages(self, help_command): - """Test _send_pages_to_dm with three pages for complete pagination.""" + async def test_send_pages_to_dm_multiple_pages_view_has_correct_pages(self, help_command): + """Test that the view receives all pages.""" + pages = ["Page A", "Page B", "Page C"] help_command.paginator = MagicMock() - help_command.paginator.pages = ["Page A", "Page B", "Page C"] + help_command.paginator.pages = pages await help_command._send_pages_to_dm() - assert help_command.context.author.send.call_count == 3 + call_kwargs = help_command.context.author.send.call_args[1] + view = call_kwargs["view"] + assert view.pages is pages + assert view.author_id == 42 - expected_contents = [ - "**Page 1/3**\nPage A", - "**Page 2/3**\nPage B", - "**Page 3/3**\nPage C", - ] + @pytest.mark.asyncio + async def test_send_pages_to_dm_multiple_pages_sets_message_ref(self, help_command): + """Test that view.message is set to the sent message.""" + help_command.paginator = MagicMock() + help_command.paginator.pages = ["A", "B"] + mock_msg = AsyncMock() + help_command.context.author.send.return_value = mock_msg + + await help_command._send_pages_to_dm() - for i, expected in enumerate(expected_contents): - actual = help_command.context.author.send.call_args_list[i][0][0] - assert actual == expected + call_kwargs = help_command.context.author.send.call_args[1] + view = call_kwargs["view"] + assert view.message is mock_msg class TestSendPagesToDestination: @@ -263,11 +454,13 @@ def help_command(self): """Create a CustomHelpCommand instance.""" cmd = CustomHelpCommand() cmd.context = MagicMock() + cmd.context.author = MagicMock() + cmd.context.author.id = 42 return cmd @pytest.mark.asyncio async def test_send_pages_to_destination_single_page(self, help_command): - """Test _send_pages_to_destination with a single page does not add page header.""" + """Test _send_pages_to_destination with a single page does not add view.""" help_command.paginator = MagicMock() help_command.paginator.pages = ["```\nSingle page content\n```"] mock_destination = AsyncMock() @@ -277,42 +470,49 @@ async def test_send_pages_to_destination_single_page(self, help_command): mock_destination.send.assert_called_once_with("```\nSingle page content\n```") @pytest.mark.asyncio - async def test_send_pages_to_destination_multiple_pages(self, help_command): - """Test _send_pages_to_destination with multiple pages adds page headers.""" + async def test_send_pages_to_destination_multiple_pages_sends_single_message(self, help_command): + """Test _send_pages_to_destination with multiple pages sends one message with view.""" help_command.paginator = MagicMock() help_command.paginator.pages = ["```\nFirst page\n```", "```\nSecond page\n```"] mock_destination = AsyncMock() await help_command._send_pages_to_destination(mock_destination) - assert mock_destination.send.call_count == 2 - - first_call_content = mock_destination.send.call_args_list[0][0][0] - assert first_call_content == "**Page 1/2**\n```\nFirst page\n```" - - second_call_content = mock_destination.send.call_args_list[1][0][0] - assert second_call_content == "**Page 2/2**\n```\nSecond page\n```" + # Should send exactly one message (not two) + mock_destination.send.assert_called_once() + call_kwargs = mock_destination.send.call_args[1] + assert "**Page 1/2**" in call_kwargs["content"] + assert isinstance(call_kwargs["view"], HelpPaginatorView) @pytest.mark.asyncio - async def test_send_pages_to_destination_three_pages(self, help_command): - """Test _send_pages_to_destination with three pages for complete pagination.""" + async def test_send_pages_to_destination_multiple_pages_view_has_correct_pages(self, help_command): + """Test that the view receives all pages.""" + pages = ["Content A", "Content B", "Content C"] help_command.paginator = MagicMock() - help_command.paginator.pages = ["Content A", "Content B", "Content C"] + help_command.paginator.pages = pages mock_destination = AsyncMock() await help_command._send_pages_to_destination(mock_destination) - assert mock_destination.send.call_count == 3 + call_kwargs = mock_destination.send.call_args[1] + view = call_kwargs["view"] + assert view.pages is pages + assert view.author_id == 42 - expected_contents = [ - "**Page 1/3**\nContent A", - "**Page 2/3**\nContent B", - "**Page 3/3**\nContent C", - ] + @pytest.mark.asyncio + async def test_send_pages_to_destination_multiple_pages_sets_message_ref(self, help_command): + """Test that view.message is set to the sent message.""" + help_command.paginator = MagicMock() + help_command.paginator.pages = ["A", "B"] + mock_destination = AsyncMock() + mock_msg = AsyncMock() + mock_destination.send.return_value = mock_msg - for i, expected in enumerate(expected_contents): - actual = mock_destination.send.call_args_list[i][0][0] - assert actual == expected + await help_command._send_pages_to_destination(mock_destination) + + call_kwargs = mock_destination.send.call_args[1] + view = call_kwargs["view"] + assert view.message is mock_msg @pytest.mark.asyncio async def test_send_pages_to_destination_sends_to_correct_destination(self, help_command): @@ -327,3 +527,186 @@ async def test_send_pages_to_destination_sends_to_correct_destination(self, help destination_a.send.assert_called_once() destination_b.send.assert_not_called() + + +class TestSendBotHelp: + """Test send_bot_help method with subcommand pages.""" + + @pytest.mark.asyncio + async def test_send_bot_help_adds_subcommand_pages_for_groups(self): + """Verify groups get separate pages listing their subcommands.""" + cmd = CustomHelpCommand() + cmd.context = MagicMock() + cmd.context.author = MagicMock() + cmd.context.author.send = AsyncMock() + cmd.dm_help = False + + # Create a mock bot with a group command that has subcommands + bot = MagicMock() + bot.description = "" + cmd.context.bot = bot + + # Create a cog + mock_cog = MagicMock() + mock_cog.qualified_name = "Config" + + # Create subcommands + sub1 = MagicMock(spec=commands.Command) + sub1.name = "botgame" + sub1.short_doc = "Change bot game." + sub1.cog = mock_cog + sub1.hidden = False + sub1.checks = [] + sub1.enabled = True + + sub2 = MagicMock(spec=commands.Command) + sub2.name = "config" + sub2.short_doc = "Bot configuration." + sub2.cog = mock_cog + sub2.hidden = False + sub2.checks = [] + sub2.enabled = True + + # Create a group command with subcommands + group_cmd = MagicMock(spec=commands.Group) + group_cmd.name = "admin" + group_cmd.short_doc = "Admin commands." + group_cmd.cog = mock_cog + group_cmd.hidden = False + group_cmd.checks = [] + group_cmd.enabled = True + group_cmd.qualified_name = "admin" + group_cmd.commands = [sub1, sub2] + + # Create a regular (non-group) command + regular_cmd = MagicMock(spec=commands.Command) + regular_cmd.name = "roll" + regular_cmd.short_doc = "Roll a die." + regular_cmd.cog = mock_cog + regular_cmd.hidden = False + regular_cmd.checks = [] + regular_cmd.enabled = True + + bot.commands = [group_cmd, regular_cmd] + + # Mock send_pages to capture paginator state + sent_pages = [] + + async def capture_send_pages(): + sent_pages.extend(cmd.paginator.pages) + + cmd.send_pages = capture_send_pages + + # Mock filter_commands to return commands as-is + async def mock_filter(cmds, *, sort=True, key=None): + return sorted(cmds, key=lambda c: c.name) if sort else list(cmds) + + cmd.filter_commands = mock_filter + + mapping = {mock_cog: [group_cmd, regular_cmd], None: []} + await cmd.send_bot_help(mapping) + + # Should have at least 2 pages: overview + admin subcommands + assert len(sent_pages) >= 2 + + # First page should have the overview with command names + assert "admin" in sent_pages[0] + assert "roll" in sent_pages[0] + + # Second page should have the group subcommands + subcommand_page = sent_pages[1] + assert "admin subcommands:" in subcommand_page + assert "botgame" in subcommand_page + assert "config" in subcommand_page + + @pytest.mark.asyncio + async def test_send_bot_help_no_subcommand_page_for_empty_groups(self): + """Verify groups with no subcommands don't get extra pages.""" + cmd = CustomHelpCommand() + cmd.context = MagicMock() + cmd.context.author = MagicMock() + cmd.context.author.send = AsyncMock() + cmd.dm_help = False + + bot = MagicMock() + bot.description = "" + cmd.context.bot = bot + + mock_cog = MagicMock() + mock_cog.qualified_name = "Config" + + # Group with no subcommands + group_cmd = MagicMock(spec=commands.Group) + group_cmd.name = "admin" + group_cmd.short_doc = "Admin commands." + group_cmd.cog = mock_cog + group_cmd.hidden = False + group_cmd.checks = [] + group_cmd.enabled = True + group_cmd.qualified_name = "admin" + group_cmd.commands = [] + + bot.commands = [group_cmd] + + sent_pages = [] + + async def capture_send_pages(): + sent_pages.extend(cmd.paginator.pages) + + cmd.send_pages = capture_send_pages + + async def mock_filter(cmds, *, sort=True, key=None): + return sorted(cmds, key=lambda c: c.name) if sort else list(cmds) + + cmd.filter_commands = mock_filter + + mapping = {mock_cog: [group_cmd], None: []} + await cmd.send_bot_help(mapping) + + # Should have only the overview page, no subcommand pages + assert len(sent_pages) == 1 + + @pytest.mark.asyncio + async def test_send_bot_help_regular_commands_no_extra_pages(self): + """Verify non-group commands don't generate extra pages.""" + cmd = CustomHelpCommand() + cmd.context = MagicMock() + cmd.context.author = MagicMock() + cmd.context.author.send = AsyncMock() + cmd.dm_help = False + + bot = MagicMock() + bot.description = "" + cmd.context.bot = bot + + mock_cog = MagicMock() + mock_cog.qualified_name = "General" + + regular_cmd = MagicMock(spec=commands.Command) + regular_cmd.name = "ping" + regular_cmd.short_doc = "Pong!" + regular_cmd.cog = mock_cog + regular_cmd.hidden = False + regular_cmd.checks = [] + regular_cmd.enabled = True + + bot.commands = [regular_cmd] + + sent_pages = [] + + async def capture_send_pages(): + sent_pages.extend(cmd.paginator.pages) + + cmd.send_pages = capture_send_pages + + async def mock_filter(cmds, *, sort=True, key=None): + return sorted(cmds, key=lambda c: c.name) if sort else list(cmds) + + cmd.filter_commands = mock_filter + + mapping = {mock_cog: [regular_cmd], None: []} + await cmd.send_bot_help(mapping) + + # Only overview page + assert len(sent_pages) == 1 + assert "ping" in sent_pages[0] diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 82ff9eb9..c0c1e8a8 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -17,7 +17,7 @@ # Mock problematic imports before any test modules are loaded def setup_mocks(): """Setup mocks for problematic dependencies.""" - sys.modules['ddcDatabases'] = Mock() + sys.modules["ddcDatabases"] = Mock() def auto_import_modules(): diff --git a/tests/unit/database/test_dal.py b/tests/unit/database/test_dal.py index 31b574b8..64e59686 100644 --- a/tests/unit/database/test_dal.py +++ b/tests/unit/database/test_dal.py @@ -4,7 +4,7 @@ import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch -sys.modules['ddcDatabases'] = Mock() +sys.modules["ddcDatabases"] = Mock() from src.database.dal.bot.bot_configs_dal import BotConfigsDal from src.database.dal.bot.custom_commands_dal import CustomCommandsDal @@ -28,7 +28,7 @@ class TestBotConfigsDal: def mock_dal(self): db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.bot.bot_configs_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.bot.bot_configs_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = BotConfigsDal(db_session, log) @@ -39,7 +39,7 @@ def test_init(self): """Test BotConfigsDal initialization.""" db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.bot.bot_configs_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.bot.bot_configs_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = BotConfigsDal(db_session, log) @@ -117,7 +117,7 @@ class TestCustomCommandsDal: def mock_dal(self): db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.bot.custom_commands_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.bot.custom_commands_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = CustomCommandsDal(db_session, log) @@ -128,7 +128,7 @@ def test_init(self): """Test CustomCommandsDal initialization.""" db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.bot.custom_commands_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.bot.custom_commands_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = CustomCommandsDal(db_session, log) @@ -233,7 +233,7 @@ class TestDiceRollsDal: def mock_dal(self): db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.bot.dice_rolls_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.bot.dice_rolls_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = DiceRollsDal(db_session, log) @@ -244,7 +244,7 @@ def test_init(self): """Test DiceRollsDal initialization.""" db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.bot.dice_rolls_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.bot.dice_rolls_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = DiceRollsDal(db_session, log) @@ -397,7 +397,7 @@ class TestProfanityFilterDal: def mock_dal(self): db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.bot.profanity_filters_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.bot.profanity_filters_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = ProfanityFilterDal(db_session, log) @@ -408,7 +408,7 @@ def test_init(self): """Test ProfanityFilterDal initialization.""" db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.bot.profanity_filters_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.bot.profanity_filters_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = ProfanityFilterDal(db_session, log) @@ -496,7 +496,7 @@ class TestServersDal: def mock_dal(self): db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.bot.servers_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.bot.servers_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = ServersDal(db_session, log) @@ -507,7 +507,7 @@ def test_init(self): """Test ServersDal initialization.""" db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.bot.servers_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.bot.servers_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = ServersDal(db_session, log) @@ -697,7 +697,7 @@ class TestGw2ConfigsDal: def mock_dal(self): db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.gw2.gw2_configs_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.gw2.gw2_configs_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = Gw2ConfigsDal(db_session, log) @@ -708,7 +708,7 @@ def test_init(self): """Test Gw2ConfigsDal initialization.""" db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.gw2.gw2_configs_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.gw2.gw2_configs_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = Gw2ConfigsDal(db_session, log) @@ -779,7 +779,7 @@ class TestGw2KeyDal: def mock_dal(self): db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.gw2.gw2_key_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.gw2.gw2_key_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = Gw2KeyDal(db_session, log) @@ -790,7 +790,7 @@ def test_init(self): """Test Gw2KeyDal initialization.""" db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.gw2.gw2_key_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.gw2.gw2_key_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = Gw2KeyDal(db_session, log) @@ -914,7 +914,7 @@ class TestGw2SessionCharsDal: def mock_dal(self): db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.gw2.gw2_session_chars_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.gw2.gw2_session_chars_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = Gw2SessionCharsDal(db_session, log) @@ -925,7 +925,7 @@ def test_init(self): """Test Gw2SessionCharsDal initialization.""" db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.gw2.gw2_session_chars_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.gw2.gw2_session_chars_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = Gw2SessionCharsDal(db_session, log) @@ -946,6 +946,8 @@ async def test_insert_session_char(self, mock_dal): "session_id": 1, "user_id": 67890, "api_key": "AAAABBBB-1111-2222-3333-444455556666", + "start": True, + "end": False, } await mock_dal.insert_session_char(gw2_api, api_characters, insert_args) @@ -975,6 +977,8 @@ async def test_insert_session_char_single_character(self, mock_dal): "session_id": 2, "user_id": 11111, "api_key": "CCCCDDDD-5555-6666-7777-888899990000", + "start": False, + "end": True, } await mock_dal.insert_session_char(gw2_api, api_characters, insert_args) @@ -1055,7 +1059,7 @@ class TestGw2SessionsDal: def mock_dal(self): db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.gw2.gw2_sessions_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.gw2.gw2_sessions_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = Gw2SessionsDal(db_session, log) @@ -1066,7 +1070,7 @@ def test_init(self): """Test Gw2SessionsDal initialization.""" db_session = MagicMock() log = MagicMock() - with patch('src.database.dal.gw2.gw2_sessions_dal.DBUtilsAsync') as mock_db_utils_class: + with patch("src.database.dal.gw2.gw2_sessions_dal.DBUtilsAsync") as mock_db_utils_class: mock_db_utils = AsyncMock() mock_db_utils_class.return_value = mock_db_utils dal = Gw2SessionsDal(db_session, log) diff --git a/tests/unit/gw2/cogs/test_account.py b/tests/unit/gw2/cogs/test_account.py index 5a6d3074..1eed5141 100644 --- a/tests/unit/gw2/cogs/test_account.py +++ b/tests/unit/gw2/cogs/test_account.py @@ -97,11 +97,11 @@ def sample_world_data(self): @pytest.mark.asyncio async def test_account_command_no_api_key(self, mock_ctx): """Test account command when user has no API key.""" - with patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.account.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.account.bot_utils.send_error_msg") as mock_error: await account(mock_ctx) mock_error.assert_called_once() @@ -111,18 +111,18 @@ async def test_account_command_no_api_key(self, mock_ctx): @pytest.mark.asyncio async def test_account_command_invalid_api_key(self, mock_ctx, sample_api_key_data): """Test account command with invalid API key.""" - with patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.account.Gw2Client') as mock_client: + with patch("src.gw2.cogs.account.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value invalid_key_error = APIInvalidKey(mock_ctx.bot, "Invalid API key") # Make the error have an args attribute like a real exception invalid_key_error.args = ("error", "Invalid API key message") mock_client_instance.check_api_key = AsyncMock(return_value=invalid_key_error) - with patch('src.gw2.cogs.account.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.account.bot_utils.send_error_msg") as mock_error: await account(mock_ctx) mock_error.assert_called_once() @@ -136,15 +136,15 @@ async def test_account_command_insufficient_permissions(self, mock_ctx, sample_a {"key": "test-api-key-12345", "permissions": "characters"} # Missing 'account' permission ] - with patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=insufficient_permissions_data) - with patch('src.gw2.cogs.account.Gw2Client') as mock_client: + with patch("src.gw2.cogs.account.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock(return_value=sample_account_data) - with patch('src.gw2.cogs.account.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.account.bot_utils.send_error_msg") as mock_error: await account(mock_ctx) mock_error.assert_called_once() @@ -156,11 +156,11 @@ async def test_account_command_successful_basic( self, mock_ctx, sample_api_key_data, sample_account_data, sample_world_data ): """Test successful account command with basic information.""" - with patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.account.Gw2Client') as mock_client: + with patch("src.gw2.cogs.account.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock(return_value=sample_account_data) mock_client_instance.call_api = AsyncMock( @@ -170,7 +170,7 @@ async def test_account_command_successful_basic( ] ) - with patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send: await account(mock_ctx) mock_send.assert_called_once() @@ -184,11 +184,11 @@ async def test_account_command_with_characters_permission(self, mock_ctx, sample characters_data = ["Character1", "Character2", "Character3"] - with patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=characters_api_key_data) - with patch('src.gw2.cogs.account.Gw2Client') as mock_client: + with patch("src.gw2.cogs.account.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock(return_value=sample_account_data) mock_client_instance.call_api = AsyncMock( @@ -199,7 +199,7 @@ async def test_account_command_with_characters_permission(self, mock_ctx, sample ] ) - with patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send: await account(mock_ctx) mock_send.assert_called_once() @@ -213,11 +213,11 @@ async def test_account_command_with_progression_permission(self, mock_ctx, sampl achievements_data = [{"id": 1, "current": 10}, {"id": 2, "current": 5}] - with patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=progression_api_key_data) - with patch('src.gw2.cogs.account.Gw2Client') as mock_client: + with patch("src.gw2.cogs.account.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock(return_value=sample_account_data) mock_client_instance.call_api = AsyncMock( @@ -228,13 +228,13 @@ async def test_account_command_with_progression_permission(self, mock_ctx, sampl ] ) - with patch('src.gw2.cogs.account.gw2_utils.calculate_user_achiev_points') as mock_calc: + with patch("src.gw2.cogs.account.gw2_utils.calculate_user_achiev_points") as mock_calc: mock_calc.return_value = 15000 - with patch('src.gw2.cogs.account.gw2_utils.get_wvw_rank_title') as mock_wvw_title: + with patch("src.gw2.cogs.account.gw2_utils.get_wvw_rank_title") as mock_wvw_title: mock_wvw_title.return_value = "Gold General" - with patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send: await account(mock_ctx) mock_send.assert_called_once() @@ -248,11 +248,11 @@ async def test_account_command_with_pvp_permission(self, mock_ctx, sample_accoun pvp_stats_data = {"pvp_rank": 45, "pvp_rank_rollovers": 5} - with patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=pvp_api_key_data) - with patch('src.gw2.cogs.account.Gw2Client') as mock_client: + with patch("src.gw2.cogs.account.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock(return_value=sample_account_data) mock_client_instance.call_api = AsyncMock( @@ -263,10 +263,10 @@ async def test_account_command_with_pvp_permission(self, mock_ctx, sample_accoun ] ) - with patch('src.gw2.cogs.account.gw2_utils.get_pvp_rank_title') as mock_pvp_title: + with patch("src.gw2.cogs.account.gw2_utils.get_pvp_rank_title") as mock_pvp_title: mock_pvp_title.return_value = "Tiger" - with patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send: await account(mock_ctx) mock_send.assert_called_once() @@ -282,11 +282,11 @@ async def test_account_command_with_guilds(self, mock_ctx, sample_account_data, guild_data_2 = {"id": "guild-id-2", "name": "Test Guild Two", "tag": "TG2"} - with patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=full_permissions_data) - with patch('src.gw2.cogs.account.Gw2Client') as mock_client: + with patch("src.gw2.cogs.account.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock(return_value=sample_account_data) mock_client_instance.call_api = AsyncMock( @@ -298,7 +298,7 @@ async def test_account_command_with_guilds(self, mock_ctx, sample_account_data, ] ) - with patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send: await account(mock_ctx) mock_send.assert_called_once() @@ -308,16 +308,16 @@ async def test_account_command_with_guilds(self, mock_ctx, sample_account_data, @pytest.mark.asyncio async def test_account_command_api_error_during_execution(self, mock_ctx, sample_api_key_data, sample_account_data): """Test account command when API error occurs during execution.""" - with patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.account.Gw2Client') as mock_client: + with patch("src.gw2.cogs.account.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock(return_value=sample_account_data) mock_client_instance.call_api = AsyncMock(side_effect=Exception("API Error")) - with patch('src.gw2.cogs.account.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.account.bot_utils.send_error_msg") as mock_error: await account(mock_ctx) mock_error.assert_called_once() @@ -328,11 +328,11 @@ async def test_account_command_guild_api_error(self, mock_ctx, sample_account_da """Test account command when guild API call fails.""" full_permissions_data = [{"key": "test-api-key-12345", "permissions": "account,guilds"}] - with patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=full_permissions_data) - with patch('src.gw2.cogs.account.Gw2Client') as mock_client: + with patch("src.gw2.cogs.account.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock(return_value=sample_account_data) mock_client_instance.call_api = AsyncMock( @@ -343,7 +343,7 @@ async def test_account_command_guild_api_error(self, mock_ctx, sample_account_da ] ) - with patch('src.gw2.cogs.account.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.account.bot_utils.send_error_msg") as mock_error: await account(mock_ctx) mock_error.assert_called_once() @@ -450,7 +450,7 @@ async def typing_side_effect(): mock_ctx.message.channel.typing = AsyncMock(side_effect=typing_side_effect) - with patch('src.gw2.cogs.account.asyncio.sleep', new_callable=AsyncMock): + with patch("src.gw2.cogs.account.asyncio.sleep", new_callable=AsyncMock): await _keep_typing_alive(mock_ctx, stop_event) assert call_count >= 1 @@ -599,15 +599,14 @@ async def test_account_command_full_success_path(self, mock_ctx, sample_account_ mock_ctx.send.return_value = progress_msg with ( - patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal, - patch('src.gw2.cogs.account.Gw2Client') as mock_client, - patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send_embed, - patch('src.gw2.cogs.account.bot_utils.get_current_date_time_str_long', return_value="2024-01-01 12:00:00"), - patch('src.gw2.cogs.account._keep_typing_alive', new=MagicMock()), - patch('src.gw2.cogs.account.asyncio.create_task') as mock_create_task, - patch('src.gw2.cogs.account.asyncio.Event') as mock_event_cls, + patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal, + patch("src.gw2.cogs.account.Gw2Client") as mock_client, + patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send_embed, + patch("src.gw2.cogs.account.bot_utils.get_current_date_time_str_long", return_value="2024-01-01 12:00:00"), + patch("src.gw2.cogs.account._keep_typing_alive", new=MagicMock()), + patch("src.gw2.cogs.account.asyncio.create_task") as mock_create_task, + patch("src.gw2.cogs.account.asyncio.Event") as mock_event_cls, ): - mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=api_key_data) mock_client_instance = mock_client.return_value @@ -654,11 +653,10 @@ async def test_account_command_exception_handler(self, mock_ctx): mock_ctx.send = AsyncMock(side_effect=RuntimeError("send failed")) with ( - patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal, - patch('src.gw2.cogs.account.Gw2Client') as mock_client, - patch('src.gw2.cogs.account.bot_utils.send_error_msg') as mock_error_msg, + patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal, + patch("src.gw2.cogs.account.Gw2Client") as mock_client, + patch("src.gw2.cogs.account.bot_utils.send_error_msg") as mock_error_msg, ): - mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=api_key_data) mock_client_instance = mock_client.return_value @@ -683,14 +681,13 @@ async def test_account_command_exception_handler_with_active_typing_task(self, m mock_ctx.send.return_value = progress_msg with ( - patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal, - patch('src.gw2.cogs.account.Gw2Client') as mock_client, - patch('src.gw2.cogs.account.bot_utils.send_error_msg') as mock_error_msg, - patch('src.gw2.cogs.account._keep_typing_alive', new=MagicMock()), - patch('src.gw2.cogs.account.asyncio.create_task') as mock_create_task, - patch('src.gw2.cogs.account.asyncio.Event') as mock_event_cls, + patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal, + patch("src.gw2.cogs.account.Gw2Client") as mock_client, + patch("src.gw2.cogs.account.bot_utils.send_error_msg") as mock_error_msg, + patch("src.gw2.cogs.account._keep_typing_alive", new=MagicMock()), + patch("src.gw2.cogs.account.asyncio.create_task") as mock_create_task, + patch("src.gw2.cogs.account.asyncio.Event") as mock_event_cls, ): - mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=api_key_data) mock_client_instance = mock_client.return_value @@ -724,22 +721,21 @@ async def test_account_command_all_permissions(self, mock_ctx, sample_account_da mock_ctx.send.return_value = progress_msg with ( - patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal, - patch('src.gw2.cogs.account.Gw2Client') as mock_client, - patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send_embed, - patch('src.gw2.cogs.account.bot_utils.get_current_date_time_str_long', return_value="2024-01-01 12:00:00"), - patch('src.gw2.cogs.account._keep_typing_alive', new=MagicMock()), - patch('src.gw2.cogs.account.asyncio.create_task') as mock_create_task, - patch('src.gw2.cogs.account.asyncio.Event') as mock_event_cls, + patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal, + patch("src.gw2.cogs.account.Gw2Client") as mock_client, + patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send_embed, + patch("src.gw2.cogs.account.bot_utils.get_current_date_time_str_long", return_value="2024-01-01 12:00:00"), + patch("src.gw2.cogs.account._keep_typing_alive", new=MagicMock()), + patch("src.gw2.cogs.account.asyncio.create_task") as mock_create_task, + patch("src.gw2.cogs.account.asyncio.Event") as mock_event_cls, patch( - 'src.gw2.cogs.account.gw2_utils.calculate_user_achiev_points', + "src.gw2.cogs.account.gw2_utils.calculate_user_achiev_points", new_callable=AsyncMock, return_value=15000, ) as mock_achiev, - patch('src.gw2.cogs.account.gw2_utils.get_wvw_rank_title', return_value="Gold General") as mock_wvw, - patch('src.gw2.cogs.account.gw2_utils.get_pvp_rank_title', return_value="Tiger") as mock_pvp, + patch("src.gw2.cogs.account.gw2_utils.get_wvw_rank_title", return_value="Gold General") as mock_wvw, + patch("src.gw2.cogs.account.gw2_utils.get_pvp_rank_title", return_value="Tiger") as mock_pvp, ): - mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=api_key_data) mock_client_instance = mock_client.return_value @@ -792,15 +788,14 @@ async def test_account_command_optional_task_failure_is_skipped( mock_ctx.send.return_value = progress_msg with ( - patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal, - patch('src.gw2.cogs.account.Gw2Client') as mock_client, - patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send_embed, - patch('src.gw2.cogs.account.bot_utils.get_current_date_time_str_long', return_value="2024-01-01 12:00:00"), - patch('src.gw2.cogs.account._keep_typing_alive', new=MagicMock()), - patch('src.gw2.cogs.account.asyncio.create_task') as mock_create_task, - patch('src.gw2.cogs.account.asyncio.Event') as mock_event_cls, + patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal, + patch("src.gw2.cogs.account.Gw2Client") as mock_client, + patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send_embed, + patch("src.gw2.cogs.account.bot_utils.get_current_date_time_str_long", return_value="2024-01-01 12:00:00"), + patch("src.gw2.cogs.account._keep_typing_alive", new=MagicMock()), + patch("src.gw2.cogs.account.asyncio.create_task") as mock_create_task, + patch("src.gw2.cogs.account.asyncio.Event") as mock_event_cls, ): - mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=api_key_data) mock_client_instance = mock_client.return_value @@ -860,15 +855,14 @@ async def test_account_command_guild_handling(self, mock_ctx, sample_world_data) mock_ctx.send.return_value = progress_msg with ( - patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal, - patch('src.gw2.cogs.account.Gw2Client') as mock_client, - patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send_embed, - patch('src.gw2.cogs.account.bot_utils.get_current_date_time_str_long', return_value="2024-01-01 12:00:00"), - patch('src.gw2.cogs.account._keep_typing_alive', new=MagicMock()), - patch('src.gw2.cogs.account.asyncio.create_task') as mock_create_task, - patch('src.gw2.cogs.account.asyncio.Event') as mock_event_cls, + patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal, + patch("src.gw2.cogs.account.Gw2Client") as mock_client, + patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send_embed, + patch("src.gw2.cogs.account.bot_utils.get_current_date_time_str_long", return_value="2024-01-01 12:00:00"), + patch("src.gw2.cogs.account._keep_typing_alive", new=MagicMock()), + patch("src.gw2.cogs.account.asyncio.create_task") as mock_create_task, + patch("src.gw2.cogs.account.asyncio.Event") as mock_event_cls, ): - mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=api_key_data) mock_client_instance = mock_client.return_value @@ -935,20 +929,19 @@ async def test_account_command_guild_fetch_exception_skipped(self, mock_ctx, sam mock_ctx.send.return_value = progress_msg with ( - patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal, - patch('src.gw2.cogs.account.Gw2Client') as mock_client, - patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send_embed, - patch('src.gw2.cogs.account.bot_utils.get_current_date_time_str_long', return_value="2024-01-01 12:00:00"), - patch('src.gw2.cogs.account._keep_typing_alive', new=MagicMock()), - patch('src.gw2.cogs.account.asyncio.create_task') as mock_create_task, - patch('src.gw2.cogs.account.asyncio.Event') as mock_event_cls, + patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal, + patch("src.gw2.cogs.account.Gw2Client") as mock_client, + patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send_embed, + patch("src.gw2.cogs.account.bot_utils.get_current_date_time_str_long", return_value="2024-01-01 12:00:00"), + patch("src.gw2.cogs.account._keep_typing_alive", new=MagicMock()), + patch("src.gw2.cogs.account.asyncio.create_task") as mock_create_task, + patch("src.gw2.cogs.account.asyncio.Event") as mock_event_cls, patch( - 'src.gw2.cogs.account._fetch_guild_info_standalone', + "src.gw2.cogs.account._fetch_guild_info_standalone", new_callable=AsyncMock, return_value=(None, "guild-id-1"), ), ): - mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=api_key_data) mock_client_instance = mock_client.return_value @@ -997,15 +990,14 @@ async def test_account_command_commander_no(self, mock_ctx, sample_world_data): mock_ctx.send.return_value = progress_msg with ( - patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal, - patch('src.gw2.cogs.account.Gw2Client') as mock_client, - patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send_embed, - patch('src.gw2.cogs.account.bot_utils.get_current_date_time_str_long', return_value="2024-01-01 12:00:00"), - patch('src.gw2.cogs.account._keep_typing_alive', new=MagicMock()), - patch('src.gw2.cogs.account.asyncio.create_task') as mock_create_task, - patch('src.gw2.cogs.account.asyncio.Event') as mock_event_cls, + patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal, + patch("src.gw2.cogs.account.Gw2Client") as mock_client, + patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send_embed, + patch("src.gw2.cogs.account.bot_utils.get_current_date_time_str_long", return_value="2024-01-01 12:00:00"), + patch("src.gw2.cogs.account._keep_typing_alive", new=MagicMock()), + patch("src.gw2.cogs.account.asyncio.create_task") as mock_create_task, + patch("src.gw2.cogs.account.asyncio.Event") as mock_event_cls, ): - mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=api_key_data) mock_client_instance = mock_client.return_value @@ -1050,15 +1042,14 @@ async def test_account_command_access_normalization(self, mock_ctx, sample_world mock_ctx.send.return_value = progress_msg with ( - patch('src.gw2.cogs.account.Gw2KeyDal') as mock_dal, - patch('src.gw2.cogs.account.Gw2Client') as mock_client, - patch('src.gw2.cogs.account.bot_utils.send_embed') as mock_send_embed, - patch('src.gw2.cogs.account.bot_utils.get_current_date_time_str_long', return_value="2024-01-01 12:00:00"), - patch('src.gw2.cogs.account._keep_typing_alive', new=MagicMock()), - patch('src.gw2.cogs.account.asyncio.create_task') as mock_create_task, - patch('src.gw2.cogs.account.asyncio.Event') as mock_event_cls, + patch("src.gw2.cogs.account.Gw2KeyDal") as mock_dal, + patch("src.gw2.cogs.account.Gw2Client") as mock_client, + patch("src.gw2.cogs.account.bot_utils.send_embed") as mock_send_embed, + patch("src.gw2.cogs.account.bot_utils.get_current_date_time_str_long", return_value="2024-01-01 12:00:00"), + patch("src.gw2.cogs.account._keep_typing_alive", new=MagicMock()), + patch("src.gw2.cogs.account.asyncio.create_task") as mock_create_task, + patch("src.gw2.cogs.account.asyncio.Event") as mock_event_cls, ): - mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=api_key_data) mock_client_instance = mock_client.return_value diff --git a/tests/unit/gw2/cogs/test_characters.py b/tests/unit/gw2/cogs/test_characters.py index e82c6682..f700f953 100644 --- a/tests/unit/gw2/cogs/test_characters.py +++ b/tests/unit/gw2/cogs/test_characters.py @@ -88,10 +88,10 @@ def sample_character_data(self): @pytest.mark.asyncio async def test_characters_no_api_key_sends_error(self, mock_ctx): """Test characters command when user has no API key.""" - with patch('src.gw2.cogs.characters.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.characters.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.characters.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.characters.bot_utils.send_error_msg") as mock_error: mock_error.return_value = None await characters(mock_ctx) mock_error.assert_called_once() @@ -101,15 +101,15 @@ async def test_characters_no_api_key_sends_error(self, mock_ctx): @pytest.mark.asyncio async def test_characters_invalid_api_key_sends_error_with_help(self, mock_ctx, sample_api_key_data): """Test characters command with invalid API key sends error with help info.""" - with patch('src.gw2.cogs.characters.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.characters.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.characters.Gw2Client') as mock_client: + with patch("src.gw2.cogs.characters.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value invalid_error = APIInvalidKey(mock_ctx.bot, "Invalid key") invalid_error.args = ("error", "This API Key is INVALID") mock_client_instance.check_api_key = AsyncMock(return_value=invalid_error) - with patch('src.gw2.cogs.characters.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.characters.bot_utils.send_error_msg") as mock_error: mock_error.return_value = None await characters(mock_ctx) mock_error.assert_called_once() @@ -123,15 +123,15 @@ async def test_characters_missing_characters_permission_sends_error(self, mock_c no_chars_permission_data = [ {"key": "test-api-key-12345", "permissions": "account,progression"} # Missing 'characters' ] - with patch('src.gw2.cogs.characters.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.characters.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=no_chars_permission_data) - with patch('src.gw2.cogs.characters.Gw2Client') as mock_client: + with patch("src.gw2.cogs.characters.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "progression"]} ) - with patch('src.gw2.cogs.characters.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.characters.bot_utils.send_error_msg") as mock_error: mock_error.return_value = None await characters(mock_ctx) mock_error.assert_called_once() @@ -144,15 +144,15 @@ async def test_characters_missing_account_permission_sends_error(self, mock_ctx) no_account_permission_data = [ {"key": "test-api-key-12345", "permissions": "characters,progression"} # Missing 'account' ] - with patch('src.gw2.cogs.characters.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.characters.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=no_account_permission_data) - with patch('src.gw2.cogs.characters.Gw2Client') as mock_client: + with patch("src.gw2.cogs.characters.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["characters", "progression"]} ) - with patch('src.gw2.cogs.characters.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.characters.bot_utils.send_error_msg") as mock_error: mock_error.return_value = None await characters(mock_ctx) mock_error.assert_called_once() @@ -164,10 +164,10 @@ async def test_characters_successful_with_character_data( self, mock_ctx, sample_api_key_data, sample_account_data, sample_character_data ): """Test successful characters command with character data creates embed with fields.""" - with patch('src.gw2.cogs.characters.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.characters.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.characters.Gw2Client') as mock_client: + with patch("src.gw2.cogs.characters.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters", "progression"]} @@ -196,8 +196,8 @@ async def test_characters_successful_with_character_data( }, ] ) - with patch('src.gw2.cogs.characters.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.characters.bot_utils.get_current_date_time_str_long') as mock_time: + with patch("src.gw2.cogs.characters.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.characters.bot_utils.get_current_date_time_str_long") as mock_time: mock_time.return_value = "2025-01-01 12:00:00" await characters(mock_ctx) mock_send.assert_called_once() @@ -222,10 +222,10 @@ async def test_characters_successful_character_age_calculation( self, mock_ctx, sample_api_key_data, sample_account_data ): """Test that character age is correctly calculated in days.""" - with patch('src.gw2.cogs.characters.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.characters.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.characters.Gw2Client') as mock_client: + with patch("src.gw2.cogs.characters.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters"]} @@ -246,8 +246,8 @@ async def test_characters_successful_character_age_calculation( }, ] ) - with patch('src.gw2.cogs.characters.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.characters.bot_utils.get_current_date_time_str_long') as mock_time: + with patch("src.gw2.cogs.characters.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.characters.bot_utils.get_current_date_time_str_long") as mock_time: mock_time.return_value = "2025-01-01 12:00:00" await characters(mock_ctx) mock_send.assert_called_once() @@ -258,10 +258,10 @@ async def test_characters_successful_character_age_calculation( @pytest.mark.asyncio async def test_characters_successful_created_date_parsed(self, mock_ctx, sample_api_key_data, sample_account_data): """Test that character created date is parsed correctly (only date portion before T).""" - with patch('src.gw2.cogs.characters.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.characters.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.characters.Gw2Client') as mock_client: + with patch("src.gw2.cogs.characters.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters"]} @@ -281,8 +281,8 @@ async def test_characters_successful_created_date_parsed(self, mock_ctx, sample_ }, ] ) - with patch('src.gw2.cogs.characters.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.characters.bot_utils.get_current_date_time_str_long') as mock_time: + with patch("src.gw2.cogs.characters.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.characters.bot_utils.get_current_date_time_str_long") as mock_time: mock_time.return_value = "2025-01-01 12:00:00" await characters(mock_ctx) mock_send.assert_called_once() @@ -293,16 +293,16 @@ async def test_characters_successful_created_date_parsed(self, mock_ctx, sample_ @pytest.mark.asyncio async def test_characters_api_exception_during_execution(self, mock_ctx, sample_api_key_data): """Test characters command when API exception occurs during execution.""" - with patch('src.gw2.cogs.characters.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.characters.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.characters.Gw2Client') as mock_client: + with patch("src.gw2.cogs.characters.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters"]} ) mock_client_instance.call_api = AsyncMock(side_effect=Exception("API connection error")) - with patch('src.gw2.cogs.characters.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.characters.bot_utils.send_error_msg") as mock_error: await characters(mock_ctx) mock_error.assert_called_once() mock_ctx.bot.log.error.assert_called_once() @@ -310,20 +310,20 @@ async def test_characters_api_exception_during_execution(self, mock_ctx, sample_ @pytest.mark.asyncio async def test_characters_triggers_typing_indicator(self, mock_ctx, sample_api_key_data): """Test that characters command triggers typing indicator.""" - with patch('src.gw2.cogs.characters.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.characters.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.characters.bot_utils.send_error_msg'): + with patch("src.gw2.cogs.characters.bot_utils.send_error_msg"): await characters(mock_ctx) mock_ctx.message.channel.typing.assert_called() @pytest.mark.asyncio async def test_characters_embed_has_thumbnail_and_author(self, mock_ctx, sample_api_key_data, sample_account_data): """Test that the characters embed has proper thumbnail and author set.""" - with patch('src.gw2.cogs.characters.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.characters.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.characters.Gw2Client') as mock_client: + with patch("src.gw2.cogs.characters.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters"]} @@ -343,8 +343,8 @@ async def test_characters_embed_has_thumbnail_and_author(self, mock_ctx, sample_ }, ] ) - with patch('src.gw2.cogs.characters.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.characters.bot_utils.get_current_date_time_str_long') as mock_time: + with patch("src.gw2.cogs.characters.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.characters.bot_utils.get_current_date_time_str_long") as mock_time: mock_time.return_value = "2025-01-01 12:00:00" await characters(mock_ctx) mock_send.assert_called_once() diff --git a/tests/unit/gw2/cogs/test_config.py b/tests/unit/gw2/cogs/test_config.py index 7d1b3ff2..b3404dfb 100644 --- a/tests/unit/gw2/cogs/test_config.py +++ b/tests/unit/gw2/cogs/test_config.py @@ -53,7 +53,7 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_config_group_invokes_subcommand(self, mock_ctx): """Test that config group command calls invoke_subcommand.""" - with patch('src.gw2.cogs.config.bot_utils.invoke_subcommand') as mock_invoke: + with patch("src.gw2.cogs.config.bot_utils.invoke_subcommand") as mock_invoke: mock_invoke.return_value = None await config(mock_ctx) mock_invoke.assert_called_once_with(mock_ctx, "gw2 config") @@ -89,12 +89,12 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_config_list_creates_embed_with_current_config_session_on(self, mock_ctx): """Test config_list creates embed with session ON.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.config.chat_formatting.green_text') as mock_green: + with patch("src.gw2.cogs.config.chat_formatting.green_text") as mock_green: mock_green.return_value = "```css\nON\n```" - with patch('src.gw2.cogs.config.chat_formatting.red_text') as mock_red: + with patch("src.gw2.cogs.config.chat_formatting.red_text") as mock_red: mock_red.return_value = "```diff\n-OFF\n```" await config_list(mock_ctx) mock_ctx.author.send.assert_called_once() @@ -108,12 +108,12 @@ async def test_config_list_creates_embed_with_current_config_session_on(self, mo @pytest.mark.asyncio async def test_config_list_creates_embed_with_current_config_session_off(self, mock_ctx): """Test config_list creates embed with session OFF.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": False}]) - with patch('src.gw2.cogs.config.chat_formatting.green_text') as mock_green: + with patch("src.gw2.cogs.config.chat_formatting.green_text") as mock_green: mock_green.return_value = "```css\nON\n```" - with patch('src.gw2.cogs.config.chat_formatting.red_text') as mock_red: + with patch("src.gw2.cogs.config.chat_formatting.red_text") as mock_red: mock_red.return_value = "```diff\n-OFF\n```" await config_list(mock_ctx) mock_ctx.author.send.assert_called_once() @@ -125,11 +125,11 @@ async def test_config_list_creates_embed_with_current_config_session_off(self, m @pytest.mark.asyncio async def test_config_list_creates_interactive_view(self, mock_ctx): """Test config_list creates interactive view with buttons.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.config.chat_formatting.green_text', return_value="ON"): - with patch('src.gw2.cogs.config.chat_formatting.red_text', return_value="OFF"): + with patch("src.gw2.cogs.config.chat_formatting.green_text", return_value="ON"): + with patch("src.gw2.cogs.config.chat_formatting.red_text", return_value="OFF"): await config_list(mock_ctx) mock_ctx.author.send.assert_called_once() call_kwargs = mock_ctx.author.send.call_args[1] @@ -139,11 +139,11 @@ async def test_config_list_creates_interactive_view(self, mock_ctx): @pytest.mark.asyncio async def test_config_list_sends_dm_and_notification_in_channel(self, mock_ctx): """Test config_list sends to DM and notification in channel.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.config.chat_formatting.green_text', return_value="ON"): - with patch('src.gw2.cogs.config.chat_formatting.red_text', return_value="OFF"): + with patch("src.gw2.cogs.config.chat_formatting.green_text", return_value="ON"): + with patch("src.gw2.cogs.config.chat_formatting.red_text", return_value="OFF"): await config_list(mock_ctx) # DM sent mock_ctx.author.send.assert_called_once() @@ -156,11 +156,11 @@ async def test_config_list_sends_dm_and_notification_in_channel(self, mock_ctx): @pytest.mark.asyncio async def test_config_list_button_style_success_when_session_on(self, mock_ctx): """Test config_list sets button style to success when session is ON.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.config.chat_formatting.green_text', return_value="ON"): - with patch('src.gw2.cogs.config.chat_formatting.red_text', return_value="OFF"): + with patch("src.gw2.cogs.config.chat_formatting.green_text", return_value="ON"): + with patch("src.gw2.cogs.config.chat_formatting.red_text", return_value="OFF"): await config_list(mock_ctx) call_kwargs = mock_ctx.author.send.call_args[1] view = call_kwargs["view"] @@ -169,11 +169,11 @@ async def test_config_list_button_style_success_when_session_on(self, mock_ctx): @pytest.mark.asyncio async def test_config_list_button_style_danger_when_session_off(self, mock_ctx): """Test config_list sets button style to danger when session is OFF.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": False}]) - with patch('src.gw2.cogs.config.chat_formatting.green_text', return_value="ON"): - with patch('src.gw2.cogs.config.chat_formatting.red_text', return_value="OFF"): + with patch("src.gw2.cogs.config.chat_formatting.green_text", return_value="ON"): + with patch("src.gw2.cogs.config.chat_formatting.red_text", return_value="OFF"): await config_list(mock_ctx) call_kwargs = mock_ctx.author.send.call_args[1] view = call_kwargs["view"] @@ -183,11 +183,11 @@ async def test_config_list_button_style_danger_when_session_off(self, mock_ctx): async def test_config_list_no_guild_icon(self, mock_ctx): """Test config_list when guild has no icon.""" mock_ctx.guild.icon = None - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.config.chat_formatting.green_text', return_value="ON"): - with patch('src.gw2.cogs.config.chat_formatting.red_text', return_value="OFF"): + with patch("src.gw2.cogs.config.chat_formatting.green_text", return_value="ON"): + with patch("src.gw2.cogs.config.chat_formatting.red_text", return_value="OFF"): await config_list(mock_ctx) call_kwargs = mock_ctx.author.send.call_args[1] embed = call_kwargs["embed"] @@ -221,10 +221,10 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_config_session_on_activates(self, mock_ctx): """Test config session 'on' activates session with green color.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.update_gw2_session_config = AsyncMock() - with patch('src.gw2.cogs.config.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.config.bot_utils.send_embed") as mock_send: await config_session(mock_ctx, "on") mock_instance.update_gw2_session_config.assert_called_once_with(99999, True, 12345) mock_send.assert_called_once() @@ -235,10 +235,10 @@ async def test_config_session_on_activates(self, mock_ctx): @pytest.mark.asyncio async def test_config_session_ON_uppercase_activates(self, mock_ctx): """Test config session 'ON' (uppercase) activates session with green color.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.update_gw2_session_config = AsyncMock() - with patch('src.gw2.cogs.config.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.config.bot_utils.send_embed") as mock_send: await config_session(mock_ctx, "ON") mock_instance.update_gw2_session_config.assert_called_once_with(99999, True, 12345) mock_send.assert_called_once() @@ -248,10 +248,10 @@ async def test_config_session_ON_uppercase_activates(self, mock_ctx): @pytest.mark.asyncio async def test_config_session_off_deactivates(self, mock_ctx): """Test config session 'off' deactivates session with red color.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.update_gw2_session_config = AsyncMock() - with patch('src.gw2.cogs.config.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.config.bot_utils.send_embed") as mock_send: await config_session(mock_ctx, "off") mock_instance.update_gw2_session_config.assert_called_once_with(99999, False, 12345) mock_send.assert_called_once() @@ -262,10 +262,10 @@ async def test_config_session_off_deactivates(self, mock_ctx): @pytest.mark.asyncio async def test_config_session_OFF_uppercase_deactivates(self, mock_ctx): """Test config session 'OFF' (uppercase) deactivates session with red color.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.update_gw2_session_config = AsyncMock() - with patch('src.gw2.cogs.config.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.config.bot_utils.send_embed") as mock_send: await config_session(mock_ctx, "OFF") mock_instance.update_gw2_session_config.assert_called_once_with(99999, False, 12345) mock_send.assert_called_once() @@ -287,10 +287,10 @@ async def test_config_session_invalid_mixed_case_raises_bad_argument(self, mock_ @pytest.mark.asyncio async def test_config_session_triggers_typing_indicator(self, mock_ctx): """Test that config session triggers typing indicator.""" - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.update_gw2_session_config = AsyncMock() - with patch('src.gw2.cogs.config.bot_utils.send_embed'): + with patch("src.gw2.cogs.config.bot_utils.send_embed"): await config_session(mock_ctx, "on") mock_ctx.message.channel.typing.assert_called_once() @@ -326,7 +326,7 @@ def server_config(self): @pytest.fixture def config_view(self, mock_ctx, server_config): """Create a GW2ConfigView instance.""" - with patch('discord.ui.view.View.__init__', return_value=None): + with patch("discord.ui.view.View.__init__", return_value=None): view = GW2ConfigView(mock_ctx, server_config) # Manually set up what View.__init__ would set view._children = [] @@ -345,7 +345,7 @@ def config_view(self, mock_ctx, server_config): @pytest.mark.asyncio async def test_config_view_initialization(self, mock_ctx, server_config): """Test GW2ConfigView initialization.""" - with patch('discord.ui.view.View.__init__', return_value=None): + with patch("discord.ui.view.View.__init__", return_value=None): view = GW2ConfigView(mock_ctx, server_config) assert view.ctx == mock_ctx assert view.server_config == server_config @@ -400,18 +400,20 @@ async def test_handle_update_successful_update(self, config_view, mock_ctx, serv button = MagicMock() - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.update_gw2_session_config = AsyncMock() - with patch('src.gw2.cogs.config.chat_formatting.green_text', return_value="ON"): - with patch('src.gw2.cogs.config.chat_formatting.red_text', return_value="OFF"): + with patch("src.gw2.cogs.config.chat_formatting.green_text", return_value="ON"): + with patch("src.gw2.cogs.config.chat_formatting.red_text", return_value="OFF"): await config_view._handle_update(interaction, button, "session", "Session Tracking") # Verify defer was called interaction.response.defer.assert_called_once() # Verify database update mock_instance.update_gw2_session_config.assert_called_once_with( - 99999, False, 12345 # Toggle from True to False + 99999, + False, + 12345, # Toggle from True to False ) # Verify config was toggled assert config_view.server_config["session"] is False @@ -431,7 +433,7 @@ async def test_handle_update_discord_http_error(self, config_view, mock_ctx): button = MagicMock() - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_response = MagicMock() mock_response.status = 500 @@ -457,7 +459,7 @@ async def test_handle_update_other_exception(self, config_view, mock_ctx): button = MagicMock() - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.update_gw2_session_config = AsyncMock(side_effect=Exception("Database connection error")) await config_view._handle_update(interaction, button, "session", "Session Tracking") @@ -482,7 +484,7 @@ async def test_handle_update_exception_with_edit_failure(self, config_view, mock button = MagicMock() - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.update_gw2_session_config = AsyncMock(side_effect=Exception("DB error")) # Should not raise - the inner exception is caught @@ -495,7 +497,7 @@ async def test_restore_buttons(self, config_view): """Test _restore_buttons restores button states.""" # Disable all buttons first for item in config_view.children: - if hasattr(item, 'disabled'): + if hasattr(item, "disabled"): item.disabled = True config_view.server_config["session"] = True @@ -503,7 +505,7 @@ async def test_restore_buttons(self, config_view): # Check buttons are re-enabled for item in config_view.children: - if hasattr(item, 'disabled'): + if hasattr(item, "disabled"): assert item.disabled is False # Check toggle_session style is success (session is True) @@ -521,9 +523,9 @@ async def test_restore_buttons_session_off(self, config_view): async def test_create_updated_embed(self, config_view): """Test _create_updated_embed creates proper embed.""" config_view.server_config["session"] = True - with patch('src.gw2.cogs.config.chat_formatting.green_text') as mock_green: + with patch("src.gw2.cogs.config.chat_formatting.green_text") as mock_green: mock_green.return_value = "```css\nON\n```" - with patch('src.gw2.cogs.config.chat_formatting.red_text') as mock_red: + with patch("src.gw2.cogs.config.chat_formatting.red_text") as mock_red: mock_red.return_value = "```diff\n-OFF\n```" embed = await config_view._create_updated_embed() assert embed.color.value == 0x00FF00 @@ -535,9 +537,9 @@ async def test_create_updated_embed(self, config_view): async def test_create_updated_embed_session_off(self, config_view): """Test _create_updated_embed with session OFF.""" config_view.server_config["session"] = False - with patch('src.gw2.cogs.config.chat_formatting.green_text') as mock_green: + with patch("src.gw2.cogs.config.chat_formatting.green_text") as mock_green: mock_green.return_value = "```css\nON\n```" - with patch('src.gw2.cogs.config.chat_formatting.red_text') as mock_red: + with patch("src.gw2.cogs.config.chat_formatting.red_text") as mock_red: mock_red.return_value = "```diff\n-OFF\n```" embed = await config_view._create_updated_embed() assert "OFF" in embed.fields[0].value @@ -547,15 +549,15 @@ async def test_create_updated_embed_no_guild_icon(self, config_view, mock_ctx): """Test _create_updated_embed when guild has no icon.""" mock_ctx.guild.icon = None config_view.server_config["session"] = True - with patch('src.gw2.cogs.config.chat_formatting.green_text', return_value="ON"): - with patch('src.gw2.cogs.config.chat_formatting.red_text', return_value="OFF"): + with patch("src.gw2.cogs.config.chat_formatting.green_text", return_value="ON"): + with patch("src.gw2.cogs.config.chat_formatting.red_text", return_value="OFF"): embed = await config_view._create_updated_embed() assert embed.thumbnail.url is None @pytest.mark.asyncio async def test_toggle_session_button_callback(self, mock_ctx, server_config): """Test toggle_session button callback calls _handle_update with correct args.""" - with patch('discord.ui.view.View.__init__', return_value=None): + with patch("discord.ui.view.View.__init__", return_value=None): view = GW2ConfigView(mock_ctx, server_config) view._children = [] view._View__timeout = 300 @@ -568,7 +570,7 @@ async def test_toggle_session_button_callback(self, mock_ctx, server_config): button = MagicMock() - with patch.object(view, '_handle_update', new_callable=AsyncMock) as mock_handle: + with patch.object(view, "_handle_update", new_callable=AsyncMock) as mock_handle: # Call the actual toggle_session function directly (it's a decorated method) await GW2ConfigView.toggle_session(view, interaction, button) mock_handle.assert_called_once_with( @@ -643,16 +645,16 @@ async def capture_disabled_state(*args, **kwargs): # Capture button states when edit is first called (processing state) if not buttons_disabled_during_processing: for item in config_view.children: - if hasattr(item, 'disabled'): + if hasattr(item, "disabled"): buttons_disabled_during_processing.append(item.disabled) interaction.edit_original_response = AsyncMock(side_effect=capture_disabled_state) - with patch('src.gw2.cogs.config.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.cogs.config.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.update_gw2_session_config = AsyncMock() - with patch('src.gw2.cogs.config.chat_formatting.green_text', return_value="ON"): - with patch('src.gw2.cogs.config.chat_formatting.red_text', return_value="OFF"): + with patch("src.gw2.cogs.config.chat_formatting.green_text", return_value="ON"): + with patch("src.gw2.cogs.config.chat_formatting.red_text", return_value="OFF"): await config_view._handle_update(interaction, button, "session", "Session Tracking") # Verify all buttons were disabled during processing diff --git a/tests/unit/gw2/cogs/test_key.py b/tests/unit/gw2/cogs/test_key.py index d8a9d238..793486d6 100644 --- a/tests/unit/gw2/cogs/test_key.py +++ b/tests/unit/gw2/cogs/test_key.py @@ -52,7 +52,7 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_key_group_invokes_subcommand(self, mock_ctx): """Test that key group command calls invoke_subcommand.""" - with patch('src.gw2.cogs.key.bot_utils.invoke_subcommand') as mock_invoke: + with patch("src.gw2.cogs.key.bot_utils.invoke_subcommand") as mock_invoke: mock_invoke.return_value = None await key(mock_ctx) mock_invoke.assert_called_once_with(mock_ctx, "gw2 key") @@ -101,13 +101,13 @@ def mock_ctx(self): async def test_add_deletes_message_for_privacy(self, mock_ctx): """Test that add command deletes the user's message for privacy.""" api_key = "test-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message') as mock_delete: - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.bot_utils.delete_message") as mock_delete: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value invalid_error = APIInvalidKey(mock_ctx.bot, "Invalid key") invalid_error.args = ("error", "Invalid API key") mock_client_instance.check_api_key = AsyncMock(return_value=invalid_error) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: mock_error.return_value = None await add(mock_ctx, api_key) mock_delete.assert_called_once_with(mock_ctx, warning=True) @@ -116,13 +116,13 @@ async def test_add_deletes_message_for_privacy(self, mock_ctx): async def test_add_invalid_api_key_sends_error(self, mock_ctx): """Test add command with invalid API key sends error message.""" api_key = "invalid-key" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value invalid_error = APIInvalidKey(mock_ctx.bot, "Invalid key") invalid_error.args = ("error", "This API Key is INVALID") mock_client_instance.check_api_key = AsyncMock(return_value=invalid_error) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: mock_error.return_value = None await add(mock_ctx, api_key) mock_error.assert_called_once() @@ -134,14 +134,14 @@ async def test_add_invalid_api_key_sends_error(self, mock_ctx): async def test_add_account_info_api_fails(self, mock_ctx): """Test add command when account info API call fails.""" api_key = "valid-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters"]} ) mock_client_instance.call_api = AsyncMock(side_effect=Exception("Account API error")) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: await add(mock_ctx, api_key) mock_error.assert_called_once() mock_ctx.bot.log.error.assert_called_once() @@ -150,8 +150,8 @@ async def test_add_account_info_api_fails(self, mock_ctx): async def test_add_server_name_api_fails(self, mock_ctx): """Test add command when server name API call fails.""" api_key = "valid-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters"]} @@ -162,7 +162,7 @@ async def test_add_server_name_api_fails(self, mock_ctx): Exception("Server API error"), # world call ] ) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: result = await add(mock_ctx, api_key) mock_error.assert_called_once() mock_ctx.bot.log.error.assert_called_once() @@ -172,8 +172,8 @@ async def test_add_server_name_api_fails(self, mock_ctx): async def test_add_user_already_has_key_shows_error_with_options(self, mock_ctx): """Test add command when user already has a key registered.""" api_key = "valid-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters"]} @@ -184,12 +184,12 @@ async def test_add_user_already_has_key_shows_error_with_options(self, mock_ctx) {"name": "Anvil Rock"}, ] ) - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock( return_value=[{"name": "OldKey", "gw2_acc_name": "TestUser.1234"}] ) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: result = await add(mock_ctx, api_key) mock_error.assert_called_once() error_msg = mock_error.call_args[0][1] @@ -203,8 +203,8 @@ async def test_add_user_already_has_key_shows_error_with_options(self, mock_ctx) async def test_add_key_already_in_use_by_someone_else(self, mock_ctx): """Test add command when the API key is already in use by another user.""" api_key = "valid-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters"]} @@ -215,11 +215,11 @@ async def test_add_key_already_in_use_by_someone_else(self, mock_ctx): {"name": "Anvil Rock"}, ] ) - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) mock_instance.get_api_key = AsyncMock(return_value=[{"user_id": 99999, "key": api_key}]) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: result = await add(mock_ctx, api_key) mock_error.assert_called_once() error_msg = mock_error.call_args[0][1] @@ -230,8 +230,8 @@ async def test_add_key_already_in_use_by_someone_else(self, mock_ctx): async def test_add_successful_insert(self, mock_ctx): """Test add command with successful key insertion.""" api_key = "valid-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters"]} @@ -242,12 +242,12 @@ async def test_add_successful_insert(self, mock_ctx): {"name": "Anvil Rock"}, ] ) - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) mock_instance.get_api_key = AsyncMock(return_value=None) mock_instance.insert_api_key = AsyncMock() - with patch('src.gw2.cogs.key.bot_utils.send_msg') as mock_send: + with patch("src.gw2.cogs.key.bot_utils.send_msg") as mock_send: result = await add(mock_ctx, api_key) mock_instance.insert_api_key.assert_called_once() insert_args = mock_instance.insert_api_key.call_args[0][0] @@ -267,8 +267,8 @@ async def test_add_successful_insert(self, mock_ctx): async def test_add_insert_raises_exception(self, mock_ctx): """Test add command when database insert raises an exception.""" api_key = "valid-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters"]} @@ -279,12 +279,12 @@ async def test_add_insert_raises_exception(self, mock_ctx): {"name": "Anvil Rock"}, ] ) - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) mock_instance.get_api_key = AsyncMock(return_value=None) mock_instance.insert_api_key = AsyncMock(side_effect=Exception("DB insert error")) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: result = await add(mock_ctx, api_key) mock_error.assert_called_once() error_msg = mock_error.call_args[0][1] @@ -329,11 +329,11 @@ def mock_ctx(self): async def test_update_deletes_message_for_privacy(self, mock_ctx): """Test that update command deletes the user's message for privacy.""" api_key = "new-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message') as mock_delete: - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.bot_utils.delete_message") as mock_delete: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg'): + with patch("src.gw2.cogs.key.bot_utils.send_error_msg"): await update(mock_ctx, api_key) mock_delete.assert_called_once_with(mock_ctx, warning=True) @@ -341,11 +341,11 @@ async def test_update_deletes_message_for_privacy(self, mock_ctx): async def test_update_no_existing_key_sends_error(self, mock_ctx): """Test update command when user has no existing key.""" api_key = "new-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: result = await update(mock_ctx, api_key) mock_error.assert_called_once() error_msg = mock_error.call_args[0][1] @@ -357,16 +357,16 @@ async def test_update_no_existing_key_sends_error(self, mock_ctx): async def test_update_invalid_api_key_sends_error(self, mock_ctx): """Test update command with invalid new API key.""" api_key = "invalid-new-key" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=[{"name": "OldKey", "key": "old-key-12345"}]) - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value invalid_error = APIInvalidKey(mock_ctx.bot, "Invalid key") invalid_error.args = ("error", "This API Key is INVALID") mock_client_instance.check_api_key = AsyncMock(return_value=invalid_error) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: mock_error.return_value = None await update(mock_ctx, api_key) mock_error.assert_called_once() @@ -377,17 +377,17 @@ async def test_update_invalid_api_key_sends_error(self, mock_ctx): async def test_update_account_info_api_fails(self, mock_ctx): """Test update command when account info API call fails.""" api_key = "new-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=[{"name": "OldKey", "key": "old-key-12345"}]) - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "NewKey", "permissions": ["account", "characters"]} ) mock_client_instance.call_api = AsyncMock(side_effect=Exception("Account API error")) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: await update(mock_ctx, api_key) mock_error.assert_called_once() mock_ctx.bot.log.error.assert_called_once() @@ -396,11 +396,11 @@ async def test_update_account_info_api_fails(self, mock_ctx): async def test_update_server_name_api_fails(self, mock_ctx): """Test update command when server name API call fails.""" api_key = "new-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=[{"name": "OldKey", "key": "old-key-12345"}]) - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "NewKey", "permissions": ["account", "characters"]} @@ -411,7 +411,7 @@ async def test_update_server_name_api_fails(self, mock_ctx): Exception("Server API error"), ] ) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: result = await update(mock_ctx, api_key) mock_error.assert_called_once() mock_ctx.bot.log.error.assert_called_once() @@ -421,14 +421,14 @@ async def test_update_server_name_api_fails(self, mock_ctx): async def test_update_key_in_use_by_different_user(self, mock_ctx): """Test update command when the new API key is in use by another user.""" api_key = "new-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=[{"name": "OldKey", "key": "old-key-12345"}]) mock_instance.get_api_key = AsyncMock( return_value=[{"user_id": 99999, "key": api_key}] # different user ) - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "NewKey", "permissions": ["account", "characters"]} @@ -439,7 +439,7 @@ async def test_update_key_in_use_by_different_user(self, mock_ctx): {"name": "Anvil Rock"}, ] ) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: result = await update(mock_ctx, api_key) mock_error.assert_called_once() error_msg = mock_error.call_args[0][1] @@ -450,13 +450,13 @@ async def test_update_key_in_use_by_different_user(self, mock_ctx): async def test_update_successful(self, mock_ctx): """Test update command with successful key update.""" api_key = "new-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=[{"name": "OldKey", "key": "old-key-12345"}]) mock_instance.get_api_key = AsyncMock(return_value=None) mock_instance.update_api_key = AsyncMock() - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "NewKey", "permissions": ["account", "characters"]} @@ -467,7 +467,7 @@ async def test_update_successful(self, mock_ctx): {"name": "Anvil Rock"}, ] ) - with patch('src.gw2.cogs.key.bot_utils.send_msg') as mock_send: + with patch("src.gw2.cogs.key.bot_utils.send_msg") as mock_send: result = await update(mock_ctx, api_key) mock_instance.update_api_key.assert_called_once() update_args = mock_instance.update_api_key.call_args[0][0] @@ -488,14 +488,14 @@ async def test_update_successful(self, mock_ctx): async def test_update_same_user_key_allowed(self, mock_ctx): """Test update command when API key is already owned by same user (re-using own key).""" api_key = "same-user-api-key" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=[{"name": "OldKey", "key": "old-key-12345"}]) # Key is found but belongs to same user mock_instance.get_api_key = AsyncMock(return_value=[{"user_id": 12345, "key": api_key}]) # same user mock_instance.update_api_key = AsyncMock() - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "NewKey", "permissions": ["account"]} @@ -506,7 +506,7 @@ async def test_update_same_user_key_allowed(self, mock_ctx): {"name": "Anvil Rock"}, ] ) - with patch('src.gw2.cogs.key.bot_utils.send_msg') as mock_send: + with patch("src.gw2.cogs.key.bot_utils.send_msg") as mock_send: result = await update(mock_ctx, api_key) mock_instance.update_api_key.assert_called_once() mock_send.assert_called_once() @@ -516,13 +516,13 @@ async def test_update_same_user_key_allowed(self, mock_ctx): async def test_update_raises_exception_on_db_update(self, mock_ctx): """Test update command when database update raises an exception.""" api_key = "new-api-key-12345" - with patch('src.gw2.cogs.key.bot_utils.delete_message'): - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.bot_utils.delete_message"): + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=[{"name": "OldKey", "key": "old-key-12345"}]) mock_instance.get_api_key = AsyncMock(return_value=None) mock_instance.update_api_key = AsyncMock(side_effect=Exception("DB update error")) - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "NewKey", "permissions": ["account"]} @@ -533,7 +533,7 @@ async def test_update_raises_exception_on_db_update(self, mock_ctx): {"name": "Anvil Rock"}, ] ) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: result = await update(mock_ctx, api_key) mock_error.assert_called_once() error_msg = mock_error.call_args[0][1] @@ -563,10 +563,10 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_remove_no_api_key_sends_error(self, mock_ctx): """Test remove command when user has no API key.""" - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: mock_error.return_value = None await remove(mock_ctx) mock_error.assert_called_once() @@ -576,11 +576,11 @@ async def test_remove_no_api_key_sends_error(self, mock_ctx): @pytest.mark.asyncio async def test_remove_has_api_key_deletes_and_confirms(self, mock_ctx): """Test remove command when user has an API key.""" - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=[{"key": "test-api-key", "name": "TestKey"}]) mock_instance.delete_user_api_key = AsyncMock() - with patch('src.gw2.cogs.key.bot_utils.send_msg') as mock_send: + with patch("src.gw2.cogs.key.bot_utils.send_msg") as mock_send: result = await remove(mock_ctx) mock_instance.delete_user_api_key.assert_called_once_with(12345) mock_send.assert_called_once() @@ -616,10 +616,10 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_info_no_api_key_sends_error(self, mock_ctx): """Test info command when user has no API key.""" - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: await info(mock_ctx) mock_error.assert_called_once() error_msg = mock_error.call_args[0][1] @@ -629,7 +629,7 @@ async def test_info_no_api_key_sends_error(self, mock_ctx): @pytest.mark.asyncio async def test_info_valid_api_key_shows_embed(self, mock_ctx): """Test info command with a valid API key shows info embed.""" - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock( return_value=[ @@ -642,13 +642,13 @@ async def test_info_valid_api_key_shows_embed(self, mock_ctx): } ] ) - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock( return_value={"name": "TestKey", "permissions": ["account", "characters", "progression"]} ) - with patch('src.gw2.cogs.key.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.key.bot_utils.get_current_date_time_str_long') as mock_time: + with patch("src.gw2.cogs.key.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.key.bot_utils.get_current_date_time_str_long") as mock_time: mock_time.return_value = "2025-01-01 12:00:00" await info(mock_ctx) mock_send.assert_called_once() @@ -661,7 +661,7 @@ async def test_info_valid_api_key_shows_embed(self, mock_ctx): @pytest.mark.asyncio async def test_info_invalid_api_key_on_check_shows_no_valid(self, mock_ctx): """Test info command when API key is invalid on check shows NO valid with invalid name.""" - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock( return_value=[ @@ -674,13 +674,13 @@ async def test_info_invalid_api_key_on_check_shows_no_valid(self, mock_ctx): } ] ) - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value invalid_error = APIInvalidKey(mock_ctx.bot, "Invalid key") invalid_error.args = ("error", "Invalid key") mock_client_instance.check_api_key = AsyncMock(return_value=invalid_error) - with patch('src.gw2.cogs.key.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.key.bot_utils.get_current_date_time_str_long') as mock_time: + with patch("src.gw2.cogs.key.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.key.bot_utils.get_current_date_time_str_long") as mock_time: mock_time.return_value = "2025-01-01 12:00:00" await info(mock_ctx) mock_send.assert_called_once() @@ -697,7 +697,7 @@ async def test_info_invalid_api_key_on_check_shows_no_valid(self, mock_ctx): @pytest.mark.asyncio async def test_info_exception_during_check(self, mock_ctx): """Test info command when an exception occurs during API key check.""" - with patch('src.gw2.cogs.key.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.key.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock( return_value=[ @@ -710,10 +710,10 @@ async def test_info_exception_during_check(self, mock_ctx): } ] ) - with patch('src.gw2.cogs.key.Gw2Client') as mock_client: + with patch("src.gw2.cogs.key.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.check_api_key = AsyncMock(side_effect=Exception("Connection error")) - with patch('src.gw2.cogs.key.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.key.bot_utils.send_error_msg") as mock_error: await info(mock_ctx) mock_error.assert_called_once() mock_ctx.bot.log.error.assert_called_once() diff --git a/tests/unit/gw2/cogs/test_misc.py b/tests/unit/gw2/cogs/test_misc.py index 6f49cc08..0702f41e 100644 --- a/tests/unit/gw2/cogs/test_misc.py +++ b/tests/unit/gw2/cogs/test_misc.py @@ -50,7 +50,7 @@ def test_gw2_misc_inheritance(self, gw2_misc_cog): def test_gw2_misc_docstring(self, gw2_misc_cog): """Test that GW2Misc has proper docstring.""" assert GW2Misc.__doc__ is not None - assert "GW2" in GW2Misc.__doc__ + assert "Guild Wars 2" in GW2Misc.__doc__ class TestWikiCommand: @@ -94,7 +94,7 @@ def _make_search_result_html(self, results): divs += ( f'
' f'{r["title"]}' - f'
\n' + f"\n" ) return f"{divs}" @@ -117,7 +117,7 @@ async def test_wiki_search_exactly_300_chars_allowed(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_error_msg', new_callable=AsyncMock) as mock_error: + with patch("src.gw2.cogs.misc.bot_utils.send_error_msg", new_callable=AsyncMock) as mock_error: await wiki(mock_ctx, search=search) # Should not send LONG_SEARCH message from src.gw2.constants import gw2_messages @@ -133,7 +133,7 @@ async def test_wiki_no_results_found(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_error_msg', new_callable=AsyncMock) as mock_error: + with patch("src.gw2.cogs.misc.bot_utils.send_error_msg", new_callable=AsyncMock) as mock_error: await wiki(mock_ctx, search="nonexistent") mock_error.assert_called_once() from src.gw2.constants import gw2_messages @@ -159,7 +159,7 @@ async def test_wiki_results_with_matching_keywords(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await wiki(mock_ctx, search="sword") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -183,7 +183,7 @@ async def test_wiki_duplicate_keyword_title_skipped(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await wiki(mock_ctx, search="eternity") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -203,7 +203,7 @@ async def test_wiki_history_page_skipped(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await wiki(mock_ctx, search="eternity") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -225,7 +225,7 @@ async def test_wiki_posts_limited_to_around_25(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await wiki(mock_ctx, search="sword") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -244,7 +244,7 @@ async def test_wiki_index_error_handled(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: # Should not raise - IndexError is caught await wiki(mock_ctx, search="test item") mock_send.assert_called_once() @@ -261,7 +261,7 @@ async def test_wiki_no_matching_keywords(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await wiki(mock_ctx, search="eternity") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -279,7 +279,7 @@ async def test_wiki_url_with_parenthesis_escaped(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await wiki(mock_ctx, search="eternity") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -298,7 +298,7 @@ async def test_wiki_sets_thumbnail(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await wiki(mock_ctx, search="eternity") embed = mock_send.call_args[0][1] from src.gw2.constants import gw2_variables @@ -316,7 +316,7 @@ async def test_wiki_search_with_spaces_converted_to_plus(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await wiki(mock_ctx, search="dawn weapon") # Verify that the URL passed to aiosession.get contains + instead of spaces called_url = mock_ctx.bot.aiosession.get.call_args[0][0] @@ -333,7 +333,7 @@ async def test_wiki_embed_title_set(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await wiki(mock_ctx, search="eternity") embed = mock_send.call_args[0][1] from src.gw2.constants import gw2_messages @@ -383,7 +383,7 @@ def _make_info_html(self, skill_name, description=None, image_alt=None, srcset=N """ blockquote = "" if description: - blockquote = f'
\n\n{description}
' + blockquote = f"
\n\n{description}
" img = "" if image_alt: @@ -405,7 +405,7 @@ async def test_info_non_200_status(self, mock_ctx): mock_response.status = 404 mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_error_msg', new_callable=AsyncMock) as mock_error: + with patch("src.gw2.cogs.misc.bot_utils.send_error_msg", new_callable=AsyncMock) as mock_error: await info(mock_ctx, skill="nonexistent") mock_error.assert_called_once() from src.gw2.constants import gw2_messages @@ -427,7 +427,7 @@ async def test_info_skill_with_description_and_icon(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="Eternity") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -452,10 +452,10 @@ async def test_info_skill_with_trading_post_data(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) tp_html = ( - '' + "" '1g' '0g 90s' - '' + "" ) mock_tp_response = AsyncMock() mock_tp_response.status = 200 @@ -469,8 +469,8 @@ async def test_info_skill_with_trading_post_data(self, mock_ctx): ] ) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: - with patch('src.gw2.cogs.misc.gw2_utils.format_gold', side_effect=["10g 0s 0c", "9g 0s 0c"]): + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.gw2_utils.format_gold", side_effect=["10g 0s 0c", "9g 0s 0c"]): await info(mock_ctx, skill="Eternity") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -483,14 +483,14 @@ async def test_info_skill_with_trading_post_data(self, mock_ctx): @pytest.mark.asyncio async def test_info_skill_without_description(self, mock_ctx): """Test info command with skill that has no description (no blockquote).""" - html = '' 'eternity.png' '' + html = 'eternity.png' mock_response = AsyncMock() mock_response.status = 200 mock_response.url = "https://wiki.guildwars2.com/wiki/Eternity" mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="Eternity") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -503,10 +503,10 @@ async def test_info_skill_without_description(self, mock_ctx): async def test_info_no_image_match_found(self, mock_ctx): """Test info command when no matching image is found.""" html = ( - '' - '
\n\nSome description.
' + "" + "
\n\nSome description.
" 'other_image.png' - '' + "" ) mock_response = AsyncMock() mock_response.status = 200 @@ -514,7 +514,7 @@ async def test_info_no_image_match_found(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="Eternity") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -524,19 +524,14 @@ async def test_info_no_image_match_found(self, mock_ctx): @pytest.mark.asyncio async def test_info_image_key_error_on_srcset(self, mock_ctx): """Test info command handles KeyError when image has no srcset.""" - html = ( - '' - '
\n\nSome description.
' - 'eternity.png' - '' - ) + html = '
\n\nSome description.
eternity.png' mock_response = AsyncMock() mock_response.status = 200 mock_response.url = "https://wiki.guildwars2.com/wiki/Eternity" mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: # Should not raise KeyError await info(mock_ctx, skill="Eternity") mock_send.assert_called_once() @@ -548,10 +543,10 @@ async def test_info_image_key_error_on_srcset(self, mock_ctx): async def test_info_no_trading_post_item(self, mock_ctx): """Test info command when no trading post item is found (item_id is None).""" html = ( - '' - '
\n\nA skill description.
' + "" + "
\n\nA skill description.
" 'eternity.png' - '' + "" ) mock_response = AsyncMock() mock_response.status = 200 @@ -559,7 +554,7 @@ async def test_info_no_trading_post_item(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="Eternity") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -592,7 +587,7 @@ async def test_info_tp_request_non_200(self, mock_ctx): ] ) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="Eternity") mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -614,7 +609,7 @@ async def test_info_skill_name_extracted_from_url(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="Sunrise") embed = mock_send.call_args[0][1] assert embed.title == "Sunrise" @@ -634,7 +629,7 @@ async def test_info_skill_name_with_underscores_converted(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="bolt of damask") embed = mock_send.call_args[0][1] assert embed.title == "Bolt of Damask" @@ -652,7 +647,7 @@ async def test_info_skill_with_spaces_replaced_by_underscore(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="dawn weapon") called_url = mock_ctx.bot.aiosession.get.call_args[0][0] assert "Dawn_Weapon" in called_url or "dawn_weapon" in called_url @@ -670,7 +665,7 @@ async def test_info_embed_author_set(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="Eternity") embed = mock_send.call_args[0][1] assert embed.author.name == "TestUser" @@ -689,7 +684,7 @@ async def test_info_embed_url_set(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="Eternity") embed = mock_send.call_args[0][1] assert embed.url == "https://wiki.guildwars2.com/wiki/Eternity" @@ -697,14 +692,14 @@ async def test_info_embed_url_set(self, mock_ctx): @pytest.mark.asyncio async def test_info_description_strips_question_mark(self, mock_ctx): """Test info command removes question marks from description.""" - html = '' '
\n\nSome description?
' '' + html = "
\n\nSome description?
" mock_response = AsyncMock() mock_response.status = 200 mock_response.url = "https://wiki.guildwars2.com/wiki/Eternity" mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="Eternity") embed = mock_send.call_args[0][1] assert "?" not in embed.description @@ -712,16 +707,14 @@ async def test_info_description_strips_question_mark(self, mock_ctx): @pytest.mark.asyncio async def test_info_description_splits_on_em_dash(self, mock_ctx): """Test info command splits description on em dash.""" - html = ( - '' '
\n\nSome description text\u2014Attribution here
' '' - ) + html = "
\n\nSome description text\u2014Attribution here
" mock_response = AsyncMock() mock_response.status = 200 mock_response.url = "https://wiki.guildwars2.com/wiki/Eternity" mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="Eternity") embed = mock_send.call_args[0][1] # Description should only contain text before the em dash @@ -743,7 +736,7 @@ async def test_info_of_and_the_lowercased_in_skill(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: await info(mock_ctx, skill="blade of the void") called_url = mock_ctx.bot.aiosession.get.call_args[0][0] # URL should use "of" and "the" (lowercase), not "Of" and "The" @@ -762,7 +755,7 @@ async def test_info_returns_none_on_success(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock): + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock): result = await info(mock_ctx, skill="Eternity") assert result is None @@ -779,7 +772,7 @@ async def test_info_calls_typing(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock): + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock): await info(mock_ctx, skill="Eternity") mock_ctx.message.channel.typing.assert_called_once() @@ -799,10 +792,10 @@ async def test_info_tp_url_format(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) tp_html = ( - '' + "" '5g' '4g 50s' - '' + "" ) mock_tp_response = AsyncMock() mock_tp_response.status = 200 @@ -815,8 +808,8 @@ async def test_info_tp_url_format(self, mock_ctx): ] ) - with patch('src.gw2.cogs.misc.bot_utils.send_embed', new_callable=AsyncMock) as mock_send: - with patch('src.gw2.cogs.misc.gw2_utils.format_gold', side_effect=["5g 0s 0c", "4g 50s 0c"]): + with patch("src.gw2.cogs.misc.bot_utils.send_embed", new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.misc.gw2_utils.format_gold", side_effect=["5g 0s 0c", "4g 50s 0c"]): await info(mock_ctx, skill="bolt of damask") embed = mock_send.call_args[0][1] # TP URL should use hyphens instead of underscores @@ -899,16 +892,16 @@ async def test_wiki_no_search_results_sends_no_results_error(self, mock_ctx): mock_response.text = AsyncMock(return_value=html) mock_ctx.bot.aiosession.get = MagicMock( return_value=type( - 'AsyncCM', + "AsyncCM", (), { - '__aenter__': AsyncMock(return_value=mock_response), - '__aexit__': AsyncMock(return_value=False), + "__aenter__": AsyncMock(return_value=mock_response), + "__aexit__": AsyncMock(return_value=False), }, )() ) - with patch('src.gw2.cogs.misc.bot_utils.send_error_msg', new_callable=AsyncMock) as mock_error: + with patch("src.gw2.cogs.misc.bot_utils.send_error_msg", new_callable=AsyncMock) as mock_error: await wiki(mock_ctx, search="xyznonexistent123") mock_error.assert_called_once() from src.gw2.constants import gw2_messages diff --git a/tests/unit/gw2/cogs/test_sessions.py b/tests/unit/gw2/cogs/test_sessions.py index 4f474cc2..b28f2a83 100644 --- a/tests/unit/gw2/cogs/test_sessions.py +++ b/tests/unit/gw2/cogs/test_sessions.py @@ -135,11 +135,11 @@ def sample_time_passed(self): @pytest.mark.asyncio async def test_session_no_api_key(self, mock_ctx): """Test session command when user has no API key.""" - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.sessions.bot_utils.send_error_msg") as mock_error: await session(mock_ctx) mock_error.assert_called_once() @@ -149,15 +149,15 @@ async def test_session_no_api_key(self, mock_ctx): @pytest.mark.asyncio async def test_session_not_active_in_config(self, mock_ctx, sample_api_key_data): """Test session command when session is not active in server config.""" - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": False}]) - with patch('src.gw2.cogs.sessions.bot_utils.send_warning_msg') as mock_warning: + with patch("src.gw2.cogs.sessions.bot_utils.send_warning_msg") as mock_warning: await session(mock_ctx) mock_warning.assert_called_once() @@ -165,15 +165,15 @@ async def test_session_not_active_in_config(self, mock_ctx, sample_api_key_data) @pytest.mark.asyncio async def test_session_not_active_empty_config(self, mock_ctx, sample_api_key_data): """Test session command when server config is empty.""" - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[]) - with patch('src.gw2.cogs.sessions.bot_utils.send_warning_msg') as mock_warning: + with patch("src.gw2.cogs.sessions.bot_utils.send_warning_msg") as mock_warning: await session(mock_ctx) mock_warning.assert_called_once() @@ -189,15 +189,15 @@ async def test_session_missing_all_permissions(self, mock_ctx): } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_no_perms) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.sessions.bot_utils.send_error_msg") as mock_error: await session(mock_ctx) mock_error.assert_called_once() @@ -215,19 +215,19 @@ async def test_session_has_some_permissions_not_all_missing(self, mock_ctx): } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_some_perms) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.sessions.bot_utils.send_error_msg") as mock_error: await session(mock_ctx) # Should not error on permissions since at least one is present @@ -237,19 +237,19 @@ async def test_session_has_some_permissions_not_all_missing(self, mock_ctx): @pytest.mark.asyncio async def test_session_no_session_found(self, mock_ctx, sample_api_key_data): """Test session command when no session records are found.""" - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.sessions.bot_utils.send_error_msg") as mock_error: await session(mock_ctx) mock_error.assert_called_once() @@ -267,19 +267,19 @@ async def test_session_end_date_is_none(self, mock_ctx, sample_api_key_data): } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.sessions.bot_utils.send_error_msg") as mock_error: await session(mock_ctx) mock_error.assert_called_once() @@ -303,25 +303,25 @@ async def test_session_time_passed_less_than_one_minute(self, mock_ctx, sample_a time_obj.minutes = 0 time_obj.seconds = 30 - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = time_obj - with patch('src.gw2.cogs.sessions.gw2_utils.send_msg') as mock_send_msg: + with patch("src.gw2.cogs.sessions.gw2_utils.send_msg") as mock_send_msg: await session(mock_ctx) mock_send_msg.assert_called_once() @@ -373,34 +373,34 @@ async def test_session_gold_gained(self, mock_ctx, sample_api_key_data, sample_t } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.gw2_utils.format_gold') as mock_format: + with patch("src.gw2.cogs.sessions.gw2_utils.format_gold") as mock_format: mock_format.return_value = "5g 00s 00c" - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`", ): await session(mock_ctx) @@ -458,34 +458,34 @@ async def test_session_gold_lost(self, mock_ctx, sample_api_key_data, sample_tim } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.gw2_utils.format_gold') as mock_format: + with patch("src.gw2.cogs.sessions.gw2_utils.format_gold") as mock_format: mock_format.return_value = "5g 00s 00c" - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`", ): await session(mock_ctx) @@ -540,35 +540,35 @@ async def test_session_gold_lost_with_leading_dash(self, mock_ctx, sample_api_ke } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.gw2_utils.format_gold') as mock_format: + with patch("src.gw2.cogs.sessions.gw2_utils.format_gold") as mock_format: # Formatted gold already starts with dash mock_format.return_value = "-5g 00s 00c" - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`", ): await session(mock_ctx) @@ -634,32 +634,32 @@ async def test_session_characters_with_deaths(self, mock_ctx, sample_api_key_dat "char2": {"name": "TestChar2", "profession": "Ranger", "deaths": 5}, # No change } - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=chars_start) mock_chars_instance.get_all_end_characters = AsyncMock(return_value=chars_end) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -718,31 +718,31 @@ async def test_session_karma_gained(self, mock_ctx, sample_api_key_data, sample_ } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -795,31 +795,31 @@ async def test_session_karma_lost(self, mock_ctx, sample_api_key_data, sample_ti } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -872,31 +872,31 @@ async def test_session_laurels_gained(self, mock_ctx, sample_api_key_data, sampl } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -951,31 +951,31 @@ async def test_session_wvw_rank_change(self, mock_ctx, sample_api_key_data, samp } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -989,34 +989,34 @@ async def test_session_wvw_rank_change(self, mock_ctx, sample_api_key_data, samp @pytest.mark.asyncio async def test_session_all_wvw_stats(self, mock_ctx, sample_api_key_data, sample_session_data, sample_time_passed): """Test session command with all WvW stats changed (yaks, players, keeps, towers, camps, castles).""" - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=sample_session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.gw2_utils.format_gold') as mock_format: + with patch("src.gw2.cogs.sessions.gw2_utils.format_gold") as mock_format: mock_format.return_value = "5g 00s 00c" - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`", ): await session(mock_ctx) @@ -1076,31 +1076,31 @@ async def test_session_wvw_tickets_gained(self, mock_ctx, sample_api_key_data, s } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -1155,31 +1155,31 @@ async def test_session_wvw_tickets_lost(self, mock_ctx, sample_api_key_data, sam } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -1234,31 +1234,31 @@ async def test_session_proof_heroics_gained(self, mock_ctx, sample_api_key_data, } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -1313,31 +1313,31 @@ async def test_session_badges_honor_gained(self, mock_ctx, sample_api_key_data, } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -1392,31 +1392,31 @@ async def test_session_guild_commendations_gained(self, mock_ctx, sample_api_key } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -1476,34 +1476,34 @@ async def test_session_still_playing_gw2(self, mock_ctx, sample_api_key_data, sa mock_ctx.message.author.activity.name = "Guild Wars 2" mock_ctx.channel = MagicMock(spec=discord.TextChannel) # Not a DMChannel - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) with patch( - 'src.gw2.cogs.sessions.gw2_utils.end_session', new_callable=AsyncMock + "src.gw2.cogs.sessions.gw2_utils.end_session", new_callable=AsyncMock ) as mock_end_session: - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`", ): await session(mock_ctx) @@ -1560,34 +1560,34 @@ async def test_session_not_playing_gw2_no_activity(self, mock_ctx, sample_api_ke mock_ctx.message.author.activity = None - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) with patch( - 'src.gw2.cogs.sessions.gw2_utils.end_session', new_callable=AsyncMock + "src.gw2.cogs.sessions.gw2_utils.end_session", new_callable=AsyncMock ) as mock_end_session: - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`", ): await session(mock_ctx) @@ -1647,34 +1647,34 @@ async def test_session_dm_channel_no_still_playing(self, mock_ctx, sample_api_ke mock_ctx.message.author.activity = MagicMock() mock_ctx.message.author.activity.name = "Guild Wars 2" - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) with patch( - 'src.gw2.cogs.sessions.gw2_utils.end_session', new_callable=AsyncMock + "src.gw2.cogs.sessions.gw2_utils.end_session", new_callable=AsyncMock ) as mock_end_session: - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`", ): await session(mock_ctx) @@ -1728,31 +1728,31 @@ async def test_session_successful_embed_basic_fields(self, mock_ctx, sample_api_ } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -1816,31 +1816,31 @@ async def test_session_time_passed_exactly_one_minute(self, mock_ctx, sample_api time_obj.seconds = 0 time_obj.timedelta = "0:01:00" - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = time_obj - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) @@ -1892,31 +1892,31 @@ async def test_session_laurels_lost(self, mock_ctx, sample_api_key_data, sample_ } ] - with patch('src.gw2.cogs.sessions.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.sessions.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=sample_api_key_data) - with patch('src.gw2.cogs.sessions.Gw2ConfigsDal') as mock_configs: + with patch("src.gw2.cogs.sessions.Gw2ConfigsDal") as mock_configs: mock_configs_instance = mock_configs.return_value mock_configs_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.cogs.sessions.Gw2SessionsDal') as mock_sessions_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionsDal") as mock_sessions_dal: mock_sessions_instance = mock_sessions_dal.return_value mock_sessions_instance.get_user_last_session = AsyncMock(return_value=session_data) - with patch('src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short') as mock_convert: + with patch("src.gw2.cogs.sessions.bot_utils.convert_str_to_datetime_short") as mock_convert: mock_convert.side_effect = lambda x: x - with patch('src.gw2.cogs.sessions.gw2_utils.get_time_passed') as mock_time: + with patch("src.gw2.cogs.sessions.gw2_utils.get_time_passed") as mock_time: mock_time.return_value = sample_time_passed - with patch('src.gw2.cogs.sessions.Gw2SessionCharsDal') as mock_chars_dal: + with patch("src.gw2.cogs.sessions.Gw2SessionCharsDal") as mock_chars_dal: mock_chars_instance = mock_chars_dal.return_value mock_chars_instance.get_all_start_characters = AsyncMock(return_value=None) - with patch('src.gw2.cogs.sessions.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.sessions.bot_utils.send_embed") as mock_send: with patch( - 'src.gw2.cogs.sessions.chat_formatting.inline', side_effect=lambda x: f"`{x}`" + "src.gw2.cogs.sessions.chat_formatting.inline", side_effect=lambda x: f"`{x}`" ): await session(mock_ctx) diff --git a/tests/unit/gw2/cogs/test_worlds.py b/tests/unit/gw2/cogs/test_worlds.py index d616bd30..169703c1 100644 --- a/tests/unit/gw2/cogs/test_worlds.py +++ b/tests/unit/gw2/cogs/test_worlds.py @@ -1,9 +1,16 @@ """Comprehensive tests for GW2 Worlds cog.""" -import asyncio import discord import pytest -from src.gw2.cogs.worlds import GW2Worlds, _send_paginated_worlds_embed, setup, worlds, worlds_eu, worlds_na +from src.gw2.cogs.worlds import ( + EmbedPaginatorView, + GW2Worlds, + _send_paginated_worlds_embed, + setup, + worlds, + worlds_eu, + worlds_na, +) from unittest.mock import AsyncMock, MagicMock, patch @@ -60,7 +67,7 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_worlds_calls_invoke_subcommand(self, mock_ctx): """Test that worlds group command calls invoke_subcommand.""" - with patch('src.gw2.cogs.worlds.bot_utils.invoke_subcommand', new_callable=AsyncMock) as mock_invoke: + with patch("src.gw2.cogs.worlds.bot_utils.invoke_subcommand", new_callable=AsyncMock) as mock_invoke: await worlds(mock_ctx) mock_invoke.assert_called_once_with(mock_ctx, "gw2 worlds") @@ -78,7 +85,6 @@ def mock_ctx(self): ctx.bot.settings = {"gw2": {"EmbedColor": 0x00FF00}} ctx.bot.user = MagicMock() ctx.bot.user.mention = "<@bot>" - ctx.bot.wait_for = AsyncMock() ctx.message = MagicMock() ctx.message.author = MagicMock() ctx.message.author.id = 12345 @@ -98,7 +104,7 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_worlds_na_get_worlds_ids_returns_false(self, mock_ctx): """Test worlds_na returns None when get_worlds_ids returns False.""" - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(False, None)) result = await worlds_na(mock_ctx) assert result is None @@ -113,12 +119,12 @@ async def test_worlds_na_successful_with_na_worlds(self, mock_ctx): ] matches_data = {"id": "1-3"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=matches_data) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_na(mock_ctx) mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -137,12 +143,12 @@ async def test_worlds_na_skips_eu_worlds(self, mock_ctx): matches_data_na = {"id": "1-2"} matches_data_eu = {"id": "2-1"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=[matches_data_na, matches_data_eu]) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_na(mock_ctx) embed = mock_send.call_args[0][1] # Only NA world should be added @@ -158,12 +164,12 @@ async def test_worlds_na_exception_on_world_logs_warning(self, mock_ctx): ] matches_data = {"id": "1-3"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=[Exception("API timeout"), matches_data]) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_na(mock_ctx) mock_ctx.bot.log.warning.assert_called_once() warning_msg = mock_ctx.bot.log.warning.call_args[0][0] @@ -182,12 +188,12 @@ async def test_worlds_na_failed_worlds_adds_footer(self, mock_ctx): {"id": 1002, "name": "Borlis Pass", "population": "Medium"}, ] - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=[Exception("Error1"), Exception("Error2")]) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_na(mock_ctx) embed = mock_send.call_args[0][1] assert embed.footer is not None @@ -204,12 +210,12 @@ async def test_worlds_na_failed_worlds_footer_truncates_at_3(self, mock_ctx): {"id": 1004, "name": "World4", "population": "VeryHigh"}, ] - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=Exception("Error")) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_na(mock_ctx) embed = mock_send.call_args[0][1] assert "..." in embed.footer.text @@ -220,12 +226,12 @@ async def test_worlds_na_calls_send_paginated(self, mock_ctx): worlds_ids = [{"id": 1001, "name": "Anvil Rock", "population": "High"}] matches_data = {"id": "1-1"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=matches_data) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_na(mock_ctx) mock_send.assert_called_once_with(mock_ctx, mock_send.call_args[0][1]) @@ -235,12 +241,12 @@ async def test_worlds_na_no_failed_worlds_no_footer(self, mock_ctx): worlds_ids = [{"id": 1001, "name": "Anvil Rock", "population": "High"}] matches_data = {"id": "1-2"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=matches_data) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_na(mock_ctx) embed = mock_send.call_args[0][1] assert embed.footer.text is None @@ -259,7 +265,6 @@ def mock_ctx(self): ctx.bot.settings = {"gw2": {"EmbedColor": 0x00FF00}} ctx.bot.user = MagicMock() ctx.bot.user.mention = "<@bot>" - ctx.bot.wait_for = AsyncMock() ctx.message = MagicMock() ctx.message.author = MagicMock() ctx.message.author.id = 12345 @@ -279,7 +284,7 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_worlds_eu_get_worlds_ids_returns_false(self, mock_ctx): """Test worlds_eu returns None when get_worlds_ids returns False.""" - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(False, None)) result = await worlds_eu(mock_ctx) assert result is None @@ -294,12 +299,12 @@ async def test_worlds_eu_successful_with_eu_worlds(self, mock_ctx): ] matches_data = {"id": "2-1"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=matches_data) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_eu(mock_ctx) mock_send.assert_called_once() embed = mock_send.call_args[0][1] @@ -318,12 +323,12 @@ async def test_worlds_eu_skips_na_worlds(self, mock_ctx): matches_data_na = {"id": "1-2"} matches_data_eu = {"id": "2-1"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=[matches_data_na, matches_data_eu]) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_eu(mock_ctx) embed = mock_send.call_args[0][1] # Only EU world should be added @@ -339,12 +344,12 @@ async def test_worlds_eu_exception_on_world_logs_warning(self, mock_ctx): ] matches_data = {"id": "2-2"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=[Exception("API timeout"), matches_data]) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_eu(mock_ctx) mock_ctx.bot.log.warning.assert_called_once() warning_msg = mock_ctx.bot.log.warning.call_args[0][0] @@ -362,12 +367,12 @@ async def test_worlds_eu_failed_worlds_adds_footer(self, mock_ctx): {"id": 2003, "name": "Gandara", "population": "VeryHigh"}, ] - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=Exception("Error")) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_eu(mock_ctx) embed = mock_send.call_args[0][1] assert embed.footer is not None @@ -384,12 +389,12 @@ async def test_worlds_eu_failed_worlds_footer_truncates_at_3(self, mock_ctx): {"id": 2005, "name": "World4", "population": "VeryHigh"}, ] - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=Exception("Error")) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_eu(mock_ctx) embed = mock_send.call_args[0][1] assert "..." in embed.footer.text @@ -400,17 +405,192 @@ async def test_worlds_eu_tier_number_replaces_2_prefix(self, mock_ctx): worlds_ids = [{"id": 2002, "name": "Desolation", "population": "Full"}] matches_data = {"id": "2-4"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=matches_data) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_eu(mock_ctx) embed = mock_send.call_args[0][1] assert "T4" in embed.fields[0].value +class TestEmbedPaginatorView: + """Test cases for the EmbedPaginatorView class.""" + + def _make_embed_pages(self, count): + """Helper to create a list of embed pages.""" + pages = [] + for i in range(count): + embed = discord.Embed(description=f"Page {i + 1}", color=0x00FF00) + embed.set_footer(text=f"Page {i + 1}/{count}") + pages.append(embed) + return pages + + @pytest.mark.asyncio + async def test_initial_state(self): + """Test view starts on page 0 with correct button states.""" + pages = self._make_embed_pages(3) + view = EmbedPaginatorView(pages, author_id=123) + + assert view.current_page == 0 + assert view.previous_button.disabled is True + assert view.next_button.disabled is False + assert view.page_indicator.label == "1/3" + assert view.page_indicator.disabled is True + + @pytest.mark.asyncio + async def test_update_buttons_middle_page(self): + """Test buttons state on middle page: both enabled.""" + pages = self._make_embed_pages(3) + view = EmbedPaginatorView(pages, author_id=1) + view.current_page = 1 + view._update_buttons() + + assert view.previous_button.disabled is False + assert view.next_button.disabled is False + assert view.page_indicator.label == "2/3" + + @pytest.mark.asyncio + async def test_update_buttons_last_page(self): + """Test buttons state on last page: next disabled.""" + pages = self._make_embed_pages(3) + view = EmbedPaginatorView(pages, author_id=1) + view.current_page = 2 + view._update_buttons() + + assert view.previous_button.disabled is False + assert view.next_button.disabled is True + assert view.page_indicator.label == "3/3" + + @pytest.mark.asyncio + async def test_next_button_advances_page(self): + """Test clicking next button advances current_page and edits embed.""" + pages = self._make_embed_pages(3) + view = EmbedPaginatorView(pages, author_id=42) + interaction = MagicMock() + interaction.user.id = 42 + interaction.response = AsyncMock() + + await view.next_button.callback(interaction) + + assert view.current_page == 1 + interaction.response.edit_message.assert_called_once() + call_kwargs = interaction.response.edit_message.call_args[1] + assert call_kwargs["embed"] is pages[1] + + @pytest.mark.asyncio + async def test_previous_button_goes_back(self): + """Test clicking previous button goes back a page.""" + pages = self._make_embed_pages(3) + view = EmbedPaginatorView(pages, author_id=42) + view.current_page = 2 + view._update_buttons() + interaction = MagicMock() + interaction.user.id = 42 + interaction.response = AsyncMock() + + await view.previous_button.callback(interaction) + + assert view.current_page == 1 + interaction.response.edit_message.assert_called_once() + call_kwargs = interaction.response.edit_message.call_args[1] + assert call_kwargs["embed"] is pages[1] + + @pytest.mark.asyncio + async def test_next_button_rejects_non_author(self): + """Test non-author clicking next gets ephemeral rejection.""" + pages = self._make_embed_pages(2) + view = EmbedPaginatorView(pages, author_id=42) + interaction = MagicMock() + interaction.user.id = 999 + interaction.response = AsyncMock() + + await view.next_button.callback(interaction) + + assert view.current_page == 0 # unchanged + interaction.response.send_message.assert_called_once_with( + "Only the command invoker can use these buttons.", ephemeral=True + ) + + @pytest.mark.asyncio + async def test_previous_button_rejects_non_author(self): + """Test non-author clicking previous gets ephemeral rejection.""" + pages = self._make_embed_pages(2) + view = EmbedPaginatorView(pages, author_id=42) + view.current_page = 1 + view._update_buttons() + interaction = MagicMock() + interaction.user.id = 999 + interaction.response = AsyncMock() + + await view.previous_button.callback(interaction) + + assert view.current_page == 1 # unchanged + interaction.response.send_message.assert_called_once_with( + "Only the command invoker can use these buttons.", ephemeral=True + ) + + @pytest.mark.asyncio + async def test_page_indicator_defers(self): + """Test page indicator button just defers (non-interactive).""" + pages = self._make_embed_pages(2) + view = EmbedPaginatorView(pages, author_id=1) + interaction = MagicMock() + interaction.response = AsyncMock() + + await view.page_indicator.callback(interaction) + + interaction.response.defer.assert_called_once() + + @pytest.mark.asyncio + async def test_on_timeout_disables_all_buttons(self): + """Test on_timeout disables all children and edits message.""" + pages = self._make_embed_pages(2) + view = EmbedPaginatorView(pages, author_id=1) + view.message = AsyncMock() + + await view.on_timeout() + + for item in view.children: + assert item.disabled is True + view.message.edit.assert_called_once_with(view=view) + + @pytest.mark.asyncio + async def test_on_timeout_no_message(self): + """Test on_timeout with no message reference does not raise.""" + pages = self._make_embed_pages(2) + view = EmbedPaginatorView(pages, author_id=1) + view.message = None + + await view.on_timeout() + + for item in view.children: + assert item.disabled is True + + @pytest.mark.asyncio + async def test_timeout_is_300(self): + """Test view timeout is 300 seconds.""" + pages = self._make_embed_pages(2) + view = EmbedPaginatorView(pages, author_id=1) + assert view.timeout == 300 + + @pytest.mark.asyncio + async def test_pages_stored(self): + """Test pages list is stored correctly.""" + pages = self._make_embed_pages(2) + view = EmbedPaginatorView(pages, author_id=1) + assert view.pages is pages + + @pytest.mark.asyncio + async def test_author_id_stored(self): + """Test author_id is stored correctly.""" + pages = self._make_embed_pages(2) + view = EmbedPaginatorView(pages, author_id=12345) + assert view.author_id == 12345 + + class TestSendPaginatedWorldsEmbed: """Test cases for the _send_paginated_worlds_embed function.""" @@ -424,7 +604,6 @@ def mock_ctx(self): ctx.bot.settings = {"gw2": {"EmbedColor": 0x00FF00}} ctx.bot.user = MagicMock() ctx.bot.user.mention = "<@bot>" - ctx.bot.wait_for = AsyncMock() ctx.message = MagicMock() ctx.message.author = MagicMock() ctx.message.author.id = 12345 @@ -467,457 +646,100 @@ async def test_sends_single_embed_exactly_25_fields(self, mock_ctx): @pytest.mark.asyncio async def test_paginates_when_more_than_25_fields(self, mock_ctx): - """Test that embed with >25 fields is paginated.""" + """Test that embed with >25 fields sends first page with view.""" embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.bot.wait_for = AsyncMock(side_effect=asyncio.TimeoutError) - await _send_paginated_worlds_embed(mock_ctx, embed) - # Should send the first page - assert mock_ctx.send.called + mock_ctx.send.assert_called_once() + call_kwargs = mock_ctx.send.call_args[1] + assert isinstance(call_kwargs["view"], EmbedPaginatorView) + sent_embed = call_kwargs["embed"] + assert len(sent_embed.fields) == 25 @pytest.mark.asyncio - async def test_dm_channel_different_footer_text(self, mock_ctx): - """Test that DM channel gets different footer text.""" + async def test_paginated_footer_shows_page_numbers(self, mock_ctx): + """Test that paginated embeds have correct page footer.""" embed = self._make_embed_with_fields(30) - mock_ctx.channel = MagicMock(spec=discord.DMChannel) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.bot.wait_for = AsyncMock(side_effect=asyncio.TimeoutError) - await _send_paginated_worlds_embed(mock_ctx, embed) - sent_embed = mock_ctx.send.call_args[1]["embed"] - assert "reactions won't disappear in DMs" in sent_embed.footer.text + call_kwargs = mock_ctx.send.call_args[1] + sent_embed = call_kwargs["embed"] + assert "Page 1/2" in sent_embed.footer.text @pytest.mark.asyncio - async def test_non_dm_channel_simple_footer(self, mock_ctx): - """Test that non-DM channel gets simple page footer.""" + async def test_paginated_view_has_second_page(self, mock_ctx): + """Test that the view contains both pages with correct fields.""" embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.id = 111 - mock_message.clear_reactions = AsyncMock() - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.bot.wait_for = AsyncMock(side_effect=asyncio.TimeoutError) - await _send_paginated_worlds_embed(mock_ctx, embed) - sent_embed = mock_ctx.send.call_args[1]["embed"] - assert "Page 1/2" in sent_embed.footer.text - assert "reactions won't disappear" not in sent_embed.footer.text + call_kwargs = mock_ctx.send.call_args[1] + view = call_kwargs["view"] + assert len(view.pages) == 2 + assert len(view.pages[0].fields) == 25 + assert len(view.pages[1].fields) == 5 + assert "Page 2/2" in view.pages[1].footer.text @pytest.mark.asyncio - async def test_single_page_after_split_sends_without_reactions(self, mock_ctx): - """Test that a single page after splitting sends without reactions.""" - # 25 fields exactly fits one page after split logic + async def test_single_page_after_split_sends_without_view(self, mock_ctx): + """Test that a single page after splitting sends without view.""" embed = self._make_embed_with_fields(25) await _send_paginated_worlds_embed(mock_ctx, embed) mock_ctx.send.assert_called_once() - - @pytest.mark.asyncio - async def test_multiple_pages_adds_reactions(self, mock_ctx): - """Test that multiple pages adds left and right arrow reactions.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.id = 111 - mock_message.clear_reactions = AsyncMock() - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.bot.wait_for = AsyncMock(side_effect=asyncio.TimeoutError) - - await _send_paginated_worlds_embed(mock_ctx, embed) - # Check that both reactions were added - calls = mock_message.add_reaction.call_args_list - emojis = [call[0][0] for call in calls] - assert "\u2b05\ufe0f" in emojis # left arrow - assert "\u27a1\ufe0f" in emojis # right arrow - - @pytest.mark.asyncio - async def test_reaction_add_fails_sends_first_page(self, mock_ctx): - """Test that when reaction add fails, first page is sent without pagination.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock(side_effect=discord.HTTPException(MagicMock(), "error")) - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - - await _send_paginated_worlds_embed(mock_ctx, embed) - # Should send twice: once for failed pagination, once for fallback - assert mock_ctx.send.call_count == 2 - - @pytest.mark.asyncio - async def test_right_arrow_next_page(self, mock_ctx): - """Test that right arrow reaction navigates to next page.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.edit = AsyncMock() - mock_message.remove_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - - # First call returns right arrow reaction, second call times out - mock_reaction = MagicMock() - mock_reaction.emoji = "\u27a1\ufe0f" - mock_reaction.message = mock_message - mock_user = MagicMock() - mock_user.bot = False - - mock_ctx.bot.wait_for = AsyncMock(side_effect=[(mock_reaction, mock_user), asyncio.TimeoutError]) - - await _send_paginated_worlds_embed(mock_ctx, embed) - # Should have edited the message to show page 2 - mock_message.edit.assert_called_once() - edited_embed = mock_message.edit.call_args[1]["embed"] - assert "Page 2/2" in edited_embed.footer.text - - @pytest.mark.asyncio - async def test_left_arrow_previous_page(self, mock_ctx): - """Test that left arrow reaction navigates to previous page.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.edit = AsyncMock() - mock_message.remove_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - - # Navigate right first, then left - mock_reaction_right = MagicMock() - mock_reaction_right.emoji = "\u27a1\ufe0f" - mock_reaction_right.message = mock_message - - mock_reaction_left = MagicMock() - mock_reaction_left.emoji = "\u2b05\ufe0f" - mock_reaction_left.message = mock_message - - mock_user = MagicMock() - mock_user.bot = False - - mock_ctx.bot.wait_for = AsyncMock( - side_effect=[ - (mock_reaction_right, mock_user), - (mock_reaction_left, mock_user), - asyncio.TimeoutError, - ] - ) - - await _send_paginated_worlds_embed(mock_ctx, embed) - # Should have edited the message twice (once right, once left) - assert mock_message.edit.call_count == 2 - # Last edit should be back to page 1 - last_edited_embed = mock_message.edit.call_args_list[1][1]["embed"] - assert "Page 1/2" in last_edited_embed.footer.text - - @pytest.mark.asyncio - async def test_left_arrow_on_first_page_does_nothing(self, mock_ctx): - """Test that left arrow on first page does not change page.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.edit = AsyncMock() - mock_message.remove_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - - mock_reaction_left = MagicMock() - mock_reaction_left.emoji = "\u2b05\ufe0f" - mock_reaction_left.message = mock_message - - mock_user = MagicMock() - mock_user.bot = False - - mock_ctx.bot.wait_for = AsyncMock(side_effect=[(mock_reaction_left, mock_user), asyncio.TimeoutError]) - - await _send_paginated_worlds_embed(mock_ctx, embed) - # Should not edit message since already on first page - mock_message.edit.assert_not_called() - - @pytest.mark.asyncio - async def test_right_arrow_on_last_page_does_nothing(self, mock_ctx): - """Test that right arrow on last page does not change page.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.edit = AsyncMock() - mock_message.remove_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - - mock_reaction_right = MagicMock() - mock_reaction_right.emoji = "\u27a1\ufe0f" - mock_reaction_right.message = mock_message - - mock_user = MagicMock() - mock_user.bot = False - - # Go right twice (only first should work since only 2 pages) - mock_ctx.bot.wait_for = AsyncMock( - side_effect=[ - (mock_reaction_right, mock_user), - (mock_reaction_right, mock_user), - asyncio.TimeoutError, - ] - ) - - await _send_paginated_worlds_embed(mock_ctx, embed) - # Only one edit (the first right arrow) - assert mock_message.edit.call_count == 1 - - @pytest.mark.asyncio - async def test_not_in_dm_removes_user_reaction(self, mock_ctx): - """Test that in non-DM channel, user reaction is removed.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.edit = AsyncMock() - mock_message.remove_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - # Non-DM channel (TextChannel) - mock_ctx.channel = MagicMock(spec=discord.TextChannel) - - mock_reaction = MagicMock() - mock_reaction.emoji = "\u27a1\ufe0f" - mock_reaction.message = mock_message - - mock_user = MagicMock() - mock_user.bot = False - - mock_ctx.bot.wait_for = AsyncMock(side_effect=[(mock_reaction, mock_user), asyncio.TimeoutError]) - - await _send_paginated_worlds_embed(mock_ctx, embed) - mock_message.remove_reaction.assert_called_once_with(mock_reaction.emoji, mock_user) - - @pytest.mark.asyncio - async def test_in_dm_skips_reaction_removal(self, mock_ctx): - """Test that in DM channel, user reaction is NOT removed.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.edit = AsyncMock() - mock_message.remove_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.channel = MagicMock(spec=discord.DMChannel) - - mock_reaction = MagicMock() - mock_reaction.emoji = "\u27a1\ufe0f" - mock_reaction.message = mock_message - - mock_user = MagicMock() - mock_user.bot = False - - mock_ctx.bot.wait_for = AsyncMock(side_effect=[(mock_reaction, mock_user), asyncio.TimeoutError]) - - await _send_paginated_worlds_embed(mock_ctx, embed) - mock_message.remove_reaction.assert_not_called() - - @pytest.mark.asyncio - async def test_timeout_error_silently_passes(self, mock_ctx): - """Test that TimeoutError is handled silently.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.bot.wait_for = AsyncMock(side_effect=asyncio.TimeoutError) - - # Should not raise - await _send_paginated_worlds_embed(mock_ctx, embed) - - @pytest.mark.asyncio - async def test_not_in_dm_after_timeout_clears_reactions(self, mock_ctx): - """Test that reactions are cleared after timeout in non-DM channel.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.channel = MagicMock(spec=discord.TextChannel) - mock_ctx.bot.wait_for = AsyncMock(side_effect=asyncio.TimeoutError) - - await _send_paginated_worlds_embed(mock_ctx, embed) - mock_message.clear_reactions.assert_called_once() - - @pytest.mark.asyncio - async def test_in_dm_after_timeout_does_not_clear_reactions(self, mock_ctx): - """Test that reactions are NOT cleared after timeout in DM channel.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.channel = MagicMock(spec=discord.DMChannel) - mock_ctx.bot.wait_for = AsyncMock(side_effect=asyncio.TimeoutError) - - await _send_paginated_worlds_embed(mock_ctx, embed) - mock_message.clear_reactions.assert_not_called() - - @pytest.mark.asyncio - async def test_forbidden_on_clear_reactions_silently_passes(self, mock_ctx): - """Test that Forbidden error on clear_reactions is silently handled.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock(side_effect=discord.Forbidden(MagicMock(), "Missing permissions")) - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.channel = MagicMock(spec=discord.TextChannel) - mock_ctx.bot.wait_for = AsyncMock(side_effect=asyncio.TimeoutError) - - # Should not raise - await _send_paginated_worlds_embed(mock_ctx, embed) - - @pytest.mark.asyncio - async def test_remove_reaction_forbidden_silently_passes(self, mock_ctx): - """Test that Forbidden on remove_reaction is silently handled.""" - embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.edit = AsyncMock() - mock_message.remove_reaction = AsyncMock(side_effect=discord.Forbidden(MagicMock(), "Missing permissions")) - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.channel = MagicMock(spec=discord.TextChannel) - - mock_reaction = MagicMock() - mock_reaction.emoji = "\u27a1\ufe0f" - mock_reaction.message = mock_message - - mock_user = MagicMock() - mock_user.bot = False - - mock_ctx.bot.wait_for = AsyncMock(side_effect=[(mock_reaction, mock_user), asyncio.TimeoutError]) - - # Should not raise - await _send_paginated_worlds_embed(mock_ctx, embed) + call_kwargs = mock_ctx.send.call_args[1] + assert "view" not in call_kwargs @pytest.mark.asyncio async def test_embed_description_preserved_in_pages(self, mock_ctx): """Test that embed description is preserved in paginated pages.""" embed = self._make_embed_with_fields(30, description="~~~~~ NA Servers ~~~~~") - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.bot.wait_for = AsyncMock(side_effect=asyncio.TimeoutError) - await _send_paginated_worlds_embed(mock_ctx, embed) - sent_embed = mock_ctx.send.call_args[1]["embed"] + call_kwargs = mock_ctx.send.call_args[1] + sent_embed = call_kwargs["embed"] assert sent_embed.description == "~~~~~ NA Servers ~~~~~" @pytest.mark.asyncio async def test_color_applied_to_all_pages(self, mock_ctx): """Test that color is applied to paginated pages.""" embed = self._make_embed_with_fields(30) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.edit = AsyncMock() - mock_message.remove_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - - mock_reaction = MagicMock() - mock_reaction.emoji = "\u27a1\ufe0f" - mock_reaction.message = mock_message - mock_user = MagicMock() - mock_user.bot = False - - mock_ctx.bot.wait_for = AsyncMock(side_effect=[(mock_reaction, mock_user), asyncio.TimeoutError]) - await _send_paginated_worlds_embed(mock_ctx, embed) - # First page - first_page = mock_ctx.send.call_args[1]["embed"] - assert first_page.color.value == 0x00FF00 - # Second page - second_page = mock_message.edit.call_args[1]["embed"] - assert second_page.color.value == 0x00FF00 + call_kwargs = mock_ctx.send.call_args[1] + view = call_kwargs["view"] + for page in view.pages: + assert page.color.value == 0x00FF00 @pytest.mark.asyncio async def test_fields_split_correctly_across_pages(self, mock_ctx): """Test that fields are correctly distributed across pages.""" embed = self._make_embed_with_fields(55) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.edit = AsyncMock() - mock_message.remove_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - - mock_reaction = MagicMock() - mock_reaction.emoji = "\u27a1\ufe0f" - mock_reaction.message = mock_message - mock_user = MagicMock() - mock_user.bot = False - - mock_ctx.bot.wait_for = AsyncMock( - side_effect=[ - (mock_reaction, mock_user), - (mock_reaction, mock_user), - asyncio.TimeoutError, - ] - ) - await _send_paginated_worlds_embed(mock_ctx, embed) - # First page should have 25 fields - first_page = mock_ctx.send.call_args[1]["embed"] - assert len(first_page.fields) == 25 - # Page 1/3 - assert "Page 1/3" in first_page.footer.text - - -class TestWorldsSetup: - """Test cases for worlds cog setup.""" - - @pytest.mark.asyncio - async def test_setup_function_exists(self): - """Test that setup function exists and is callable.""" - assert callable(setup) + call_kwargs = mock_ctx.send.call_args[1] + view = call_kwargs["view"] + assert len(view.pages) == 3 + assert len(view.pages[0].fields) == 25 + assert len(view.pages[1].fields) == 25 + assert len(view.pages[2].fields) == 5 + assert "Page 1/3" in view.pages[0].footer.text @pytest.mark.asyncio - async def test_setup_removes_existing_gw2_command(self): - """Test that setup removes existing gw2 command.""" - mock_bot = MagicMock() - mock_bot.remove_command = MagicMock() - mock_bot.add_cog = AsyncMock() - - await setup(mock_bot) - mock_bot.remove_command.assert_called_once_with("gw2") + async def test_view_message_reference_is_set(self, mock_ctx): + """Test that view.message is set to the sent message.""" + embed = self._make_embed_with_fields(30) + mock_msg = AsyncMock() + mock_ctx.send.return_value = mock_msg + await _send_paginated_worlds_embed(mock_ctx, embed) + call_kwargs = mock_ctx.send.call_args[1] + view = call_kwargs["view"] + assert view.message is mock_msg @pytest.mark.asyncio - async def test_setup_adds_cog(self): - """Test that setup adds the GW2Worlds cog.""" - mock_bot = MagicMock() - mock_bot.remove_command = MagicMock() - mock_bot.add_cog = AsyncMock() - - await setup(mock_bot) - mock_bot.add_cog.assert_called_once() - cog_instance = mock_bot.add_cog.call_args[0][0] - assert isinstance(cog_instance, GW2Worlds) + async def test_view_author_id_matches_ctx_author(self, mock_ctx): + """Test that view.author_id matches ctx.author.id.""" + embed = self._make_embed_with_fields(30) + await _send_paginated_worlds_embed(mock_ctx, embed) + call_kwargs = mock_ctx.send.call_args[1] + view = call_kwargs["view"] + assert view.author_id == 12345 class TestSendPaginatedWorldsEmbedEdgeCases: - """Additional edge case tests for _send_paginated_worlds_embed (lines 154-155, 177).""" + """Additional edge case tests for _send_paginated_worlds_embed.""" @pytest.fixture def mock_ctx(self): @@ -929,7 +751,6 @@ def mock_ctx(self): ctx.bot.settings = {"gw2": {"EmbedColor": 0x00FF00}} ctx.bot.user = MagicMock() ctx.bot.user.mention = "<@bot>" - ctx.bot.wait_for = AsyncMock() ctx.message = MagicMock() ctx.message.author = MagicMock() ctx.message.author.id = 12345 @@ -952,9 +773,7 @@ def _make_embed_with_fields(self, num_fields, description="Test"): @pytest.mark.asyncio async def test_single_page_after_split_sends_directly(self, mock_ctx): - """Test that when fields split into exactly one page, it sends without reactions (lines 153-155).""" - # 26 fields -> split gives: page1=25, page2=1 -> 2 pages - # But 25 fields -> split gives 1 page only since len<=25 + """Test that when fields fit in one page, it sends without view.""" embed = self._make_embed_with_fields(25) await _send_paginated_worlds_embed(mock_ctx, embed) mock_ctx.send.assert_called_once() @@ -963,36 +782,31 @@ async def test_single_page_after_split_sends_directly(self, mock_ctx): @pytest.mark.asyncio async def test_paginated_26_fields_creates_two_pages(self, mock_ctx): - """Test pagination with 26 fields creates exactly 2 pages (lines 153-155).""" + """Test pagination with 26 fields creates exactly 2 pages.""" embed = self._make_embed_with_fields(26) - mock_message = MagicMock() - mock_message.add_reaction = AsyncMock() - mock_message.clear_reactions = AsyncMock() - mock_message.id = 111 - mock_ctx.send = AsyncMock(return_value=mock_message) - mock_ctx.bot.wait_for = AsyncMock(side_effect=asyncio.TimeoutError) - await _send_paginated_worlds_embed(mock_ctx, embed) - # Should send first page - sent_embed = mock_ctx.send.call_args[1]["embed"] - assert len(sent_embed.fields) == 25 - assert "Page 1/2" in sent_embed.footer.text + call_kwargs = mock_ctx.send.call_args[1] + view = call_kwargs["view"] + assert len(view.pages) == 2 + assert len(view.pages[0].fields) == 25 + assert len(view.pages[1].fields) == 1 + assert "Page 1/2" in view.pages[0].footer.text @pytest.mark.asyncio async def test_worlds_na_exact_2001_boundary(self, mock_ctx): - """Test worlds_na does not include world ID exactly at 2001 (line 177 equivalence).""" + """Test worlds_na does not include world ID exactly at 2001.""" worlds_ids = [ {"id": 2001, "name": "Boundary World", "population": "High"}, ] # wid=2001 is NOT < 2001, so NA should NOT add it matches_data = {"id": "2-1"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=matches_data) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_na(mock_ctx) embed = mock_send.call_args[0][1] # 2001 is NOT < 2001, so should not be added to NA embed @@ -1000,20 +814,51 @@ async def test_worlds_na_exact_2001_boundary(self, mock_ctx): @pytest.mark.asyncio async def test_worlds_eu_exact_2001_boundary(self, mock_ctx): - """Test worlds_eu does not include world ID exactly at 2001 (line 177).""" + """Test worlds_eu does not include world ID exactly at 2001.""" worlds_ids = [ {"id": 2001, "name": "Boundary World", "population": "High"}, ] # wid=2001 is NOT > 2001, so EU should NOT add it matches_data = {"id": "2-1"} - with patch('src.gw2.cogs.worlds.gw2_utils') as mock_utils: + with patch("src.gw2.cogs.worlds.gw2_utils") as mock_utils: mock_utils.get_worlds_ids = AsyncMock(return_value=(True, worlds_ids)) - with patch('src.gw2.cogs.worlds.Gw2Client') as mock_client: + with patch("src.gw2.cogs.worlds.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=matches_data) - with patch('src.gw2.cogs.worlds._send_paginated_worlds_embed', new_callable=AsyncMock) as mock_send: + with patch("src.gw2.cogs.worlds._send_paginated_worlds_embed", new_callable=AsyncMock) as mock_send: await worlds_eu(mock_ctx) embed = mock_send.call_args[0][1] # 2001 is NOT > 2001, so should not be added to EU embed assert len(embed.fields) == 0 + + +class TestWorldsSetup: + """Test cases for worlds cog setup.""" + + @pytest.mark.asyncio + async def test_setup_function_exists(self): + """Test that setup function exists and is callable.""" + assert callable(setup) + + @pytest.mark.asyncio + async def test_setup_removes_existing_gw2_command(self): + """Test that setup removes existing gw2 command.""" + mock_bot = MagicMock() + mock_bot.remove_command = MagicMock() + mock_bot.add_cog = AsyncMock() + + await setup(mock_bot) + mock_bot.remove_command.assert_called_once_with("gw2") + + @pytest.mark.asyncio + async def test_setup_adds_cog(self): + """Test that setup adds the GW2Worlds cog.""" + mock_bot = MagicMock() + mock_bot.remove_command = MagicMock() + mock_bot.add_cog = AsyncMock() + + await setup(mock_bot) + mock_bot.add_cog.assert_called_once() + cog_instance = mock_bot.add_cog.call_args[0][0] + assert isinstance(cog_instance, GW2Worlds) diff --git a/tests/unit/gw2/cogs/test_wvw.py b/tests/unit/gw2/cogs/test_wvw.py index 8416e09c..dfd189f1 100644 --- a/tests/unit/gw2/cogs/test_wvw.py +++ b/tests/unit/gw2/cogs/test_wvw.py @@ -7,6 +7,7 @@ _get_kdr_embed_values, _get_map_names_embed_values, _get_match_embed_values, + _resolve_tier, setup, ) from src.gw2.tools.gw2_exceptions import APIKeyError @@ -149,12 +150,12 @@ async def test_info_no_world_no_api_key(self, mock_bot, mock_ctx): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: - with patch('src.gw2.cogs.wvw.Gw2Client'): + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: + with patch("src.gw2.cogs.wvw.Gw2Client"): await cog.info.callback(cog, mock_ctx, world=None) mock_error.assert_called_once() @@ -169,11 +170,11 @@ async def test_info_no_world_api_key_exists(self, mock_bot, mock_ctx, sample_mat api_key_data = [{"key": "test-api-key-12345"}] - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_data) - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -183,7 +184,7 @@ async def test_info_no_world_api_key_exists(self, mock_bot, mock_ctx, sample_mat ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.info.callback(cog, mock_ctx, world=None) mock_send.assert_called_once() @@ -195,15 +196,15 @@ async def test_info_no_world_api_key_error(self, mock_bot, mock_ctx): api_key_data = [{"key": "test-api-key-12345"}] - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_data) - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=APIKeyError(mock_ctx.bot, "Invalid key")) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.info.callback(cog, mock_ctx, world=None) mock_error.assert_called_once() @@ -218,16 +219,16 @@ async def test_info_no_world_generic_exception(self, mock_bot, mock_ctx): api_key_data = [{"key": "test-api-key-12345"}] - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_data) - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value error = Exception("Something went wrong") mock_client_instance.call_api = AsyncMock(side_effect=error) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.info.callback(cog, mock_ctx, world=None) mock_error.assert_called_once_with(mock_ctx, error) @@ -241,10 +242,10 @@ async def test_info_world_given_calls_get_world_id( cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -253,7 +254,7 @@ async def test_info_world_given_calls_get_world_id( ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.info.callback(cog, mock_ctx, world="Anvil Rock") mock_get_wid.assert_called_once_with(mock_ctx.bot, "Anvil Rock") @@ -265,11 +266,11 @@ async def test_info_wid_is_none(self, mock_bot, mock_ctx): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = None - with patch('src.gw2.cogs.wvw.Gw2Client'): - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.Gw2Client"): + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.info.callback(cog, mock_ctx, world="InvalidWorld") mock_error.assert_called_once() @@ -283,15 +284,15 @@ async def test_info_api_call_fails(self, mock_bot, mock_ctx): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value error = Exception("API failure") mock_client_instance.call_api = AsyncMock(side_effect=error) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.info.callback(cog, mock_ctx, world="Anvil Rock") mock_error.assert_called_once_with(mock_ctx, error) @@ -312,10 +313,10 @@ async def test_info_world_color_not_found(self, mock_bot, mock_ctx, sample_world }, } - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 # Not in any of the all_worlds lists - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -324,7 +325,7 @@ async def test_info_world_color_not_found(self, mock_bot, mock_ctx, sample_world ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.info.callback(cog, mock_ctx, world="Anvil Rock") mock_error.assert_called_once() @@ -337,10 +338,10 @@ async def test_info_na_tier(self, mock_bot, mock_ctx, sample_matches_data, sampl cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 # NA world - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -349,7 +350,7 @@ async def test_info_na_tier(self, mock_bot, mock_ctx, sample_matches_data, sampl ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.info.callback(cog, mock_ctx, world="Anvil Rock") embed = mock_send.call_args[0][1] @@ -387,10 +388,10 @@ async def test_info_eu_tier(self, mock_bot, mock_ctx, sample_worldinfo_data): "population": "Full", } - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 2001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -399,7 +400,7 @@ async def test_info_eu_tier(self, mock_bot, mock_ctx, sample_worldinfo_data): ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.info.callback(cog, mock_ctx, world="Desolation") embed = mock_send.call_args[0][1] @@ -411,10 +412,10 @@ async def test_info_red_world_color(self, mock_bot, mock_ctx, sample_matches_dat cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 # In red all_worlds - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -423,7 +424,7 @@ async def test_info_red_world_color(self, mock_bot, mock_ctx, sample_matches_dat ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.info.callback(cog, mock_ctx, world="Anvil Rock") embed = mock_send.call_args[0][1] @@ -435,10 +436,10 @@ async def test_info_green_world_color(self, mock_bot, mock_ctx, sample_matches_d cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1003 # In green all_worlds - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -447,7 +448,7 @@ async def test_info_green_world_color(self, mock_bot, mock_ctx, sample_matches_d ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.info.callback(cog, mock_ctx, world="Some World") embed = mock_send.call_args[0][1] @@ -459,10 +460,10 @@ async def test_info_blue_world_color(self, mock_bot, mock_ctx, sample_matches_da cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1005 # In blue all_worlds - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -471,7 +472,7 @@ async def test_info_blue_world_color(self, mock_bot, mock_ctx, sample_matches_da ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.info.callback(cog, mock_ctx, world="Some World") embed = mock_send.call_args[0][1] @@ -489,10 +490,10 @@ async def test_info_population_veryhigh(self, mock_bot, mock_ctx, sample_matches "population": "VeryHigh", } - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -501,8 +502,8 @@ async def test_info_population_veryhigh(self, mock_bot, mock_ctx, sample_matches ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.wvw.chat_formatting.inline', side_effect=lambda x: f"`{x}`"): + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.wvw.chat_formatting.inline", side_effect=lambda x: f"`{x}`"): await cog.info.callback(cog, mock_ctx, world="Anvil Rock") embed = mock_send.call_args[0][1] @@ -535,10 +536,10 @@ async def test_info_kills_zero_kd(self, mock_bot, mock_ctx, sample_worldinfo_dat ], } - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -547,8 +548,8 @@ async def test_info_kills_zero_kd(self, mock_bot, mock_ctx, sample_worldinfo_dat ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.wvw.chat_formatting.inline', side_effect=lambda x: f"`{x}`"): + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.wvw.chat_formatting.inline", side_effect=lambda x: f"`{x}`"): await cog.info.callback(cog, mock_ctx, world="Anvil Rock") embed = mock_send.call_args[0][1] @@ -580,10 +581,10 @@ async def test_info_deaths_zero_kd(self, mock_bot, mock_ctx, sample_worldinfo_da ], } - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -592,8 +593,8 @@ async def test_info_deaths_zero_kd(self, mock_bot, mock_ctx, sample_worldinfo_da ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.wvw.chat_formatting.inline', side_effect=lambda x: f"`{x}`"): + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.wvw.chat_formatting.inline", side_effect=lambda x: f"`{x}`"): await cog.info.callback(cog, mock_ctx, world="Anvil Rock") embed = mock_send.call_args[0][1] @@ -606,10 +607,10 @@ async def test_info_normal_kd_calculation(self, mock_bot, mock_ctx, sample_match cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 # red world - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -618,8 +619,8 @@ async def test_info_normal_kd_calculation(self, mock_bot, mock_ctx, sample_match ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.wvw.chat_formatting.inline', side_effect=lambda x: f"`{x}`"): + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.wvw.chat_formatting.inline", side_effect=lambda x: f"`{x}`"): await cog.info.callback(cog, mock_ctx, world="Anvil Rock") embed = mock_send.call_args[0][1] @@ -633,10 +634,10 @@ async def test_info_successful_embed_sent(self, mock_bot, mock_ctx, sample_match cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock( side_effect=[ @@ -645,8 +646,8 @@ async def test_info_successful_embed_sent(self, mock_bot, mock_ctx, sample_match ] ) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.wvw.chat_formatting.inline', side_effect=lambda x: f"`{x}`"): + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.wvw.chat_formatting.inline", side_effect=lambda x: f"`{x}`"): await cog.info.callback(cog, mock_ctx, world="Anvil Rock") mock_send.assert_called_once() @@ -757,12 +758,12 @@ async def test_match_no_world_no_api_key(self, mock_bot, mock_ctx): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: - with patch('src.gw2.cogs.wvw.Gw2Client'): + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: + with patch("src.gw2.cogs.wvw.Gw2Client"): await cog.match.callback(cog, mock_ctx, world=None) mock_error.assert_called_once() @@ -777,20 +778,20 @@ async def test_match_no_world_api_key_error(self, mock_bot, mock_ctx): api_key_data = [{"key": "test-api-key-12345"}] - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_data) - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=APIKeyError(mock_ctx.bot, "Invalid key")) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.match.callback(cog, mock_ctx, world=None) mock_error.assert_called_once() error_msg = mock_error.call_args[0][1] - assert "You dont have an API key registered" in error_msg + assert "Missing World Name" in error_msg @pytest.mark.asyncio async def test_match_no_world_generic_exception(self, mock_bot, mock_ctx): @@ -800,16 +801,16 @@ async def test_match_no_world_generic_exception(self, mock_bot, mock_ctx): api_key_data = [{"key": "test-api-key-12345"}] - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_data) - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value error = Exception("Something went wrong") mock_client_instance.call_api = AsyncMock(side_effect=error) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.match.callback(cog, mock_ctx, world=None) mock_error.assert_called_once_with(mock_ctx, error) @@ -821,17 +822,17 @@ async def test_match_world_given_uses_get_world_id(self, mock_bot, mock_ctx, sam cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=sample_matches_data) - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)", "World2 (Medium)"] - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.match.callback(cog, mock_ctx, world="Anvil Rock") mock_get_wid.assert_called_once_with(mock_ctx.bot, "Anvil Rock") @@ -842,11 +843,11 @@ async def test_match_wid_none(self, mock_bot, mock_ctx): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = None - with patch('src.gw2.cogs.wvw.Gw2Client'): - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.Gw2Client"): + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.match.callback(cog, mock_ctx, world="InvalidWorld") mock_error.assert_called_once() @@ -860,17 +861,17 @@ async def test_match_na_tier(self, mock_bot, mock_ctx, sample_matches_data): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=sample_matches_data) - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)"] - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.match.callback(cog, mock_ctx, world="Anvil Rock") embed = mock_send.call_args[0][1] @@ -921,17 +922,17 @@ async def test_match_eu_tier(self, mock_bot, mock_ctx): ], } - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 2001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=eu_matches) - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)"] - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.match.callback(cog, mock_ctx, world="Desolation") embed = mock_send.call_args[0][1] @@ -943,15 +944,15 @@ async def test_match_exception_during_fetch(self, mock_bot, mock_ctx): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value error = Exception("API Error") mock_client_instance.call_api = AsyncMock(side_effect=error) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.match.callback(cog, mock_ctx, world="Anvil Rock") mock_error.assert_called_once_with(mock_ctx, error) @@ -963,17 +964,17 @@ async def test_match_successful_embed(self, mock_bot, mock_ctx, sample_matches_d cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=sample_matches_data) - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)", "World2 (Medium)"] - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.match.callback(cog, mock_ctx, world="Anvil Rock") mock_send.assert_called_once() @@ -1069,12 +1070,12 @@ async def test_kdr_no_world_no_api_key(self, mock_bot, mock_ctx): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=None) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: - with patch('src.gw2.cogs.wvw.Gw2Client'): + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: + with patch("src.gw2.cogs.wvw.Gw2Client"): await cog.kdr.callback(cog, mock_ctx, world=None) mock_error.assert_called_once() @@ -1089,20 +1090,20 @@ async def test_kdr_no_world_api_key_error(self, mock_bot, mock_ctx): api_key_data = [{"key": "test-api-key-12345"}] - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_data) - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=APIKeyError(mock_ctx.bot, "Invalid key")) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.kdr.callback(cog, mock_ctx, world=None) mock_error.assert_called_once() error_msg = mock_error.call_args[0][1] - assert "You dont have an API key registered" in error_msg + assert "Invalid world name" in error_msg @pytest.mark.asyncio async def test_kdr_no_world_generic_exception(self, mock_bot, mock_ctx): @@ -1112,16 +1113,16 @@ async def test_kdr_no_world_generic_exception(self, mock_bot, mock_ctx): api_key_data = [{"key": "test-api-key-12345"}] - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_data) - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value error = Exception("Something went wrong") mock_client_instance.call_api = AsyncMock(side_effect=error) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.kdr.callback(cog, mock_ctx, world=None) mock_error.assert_called_once_with(mock_ctx, error) @@ -1133,17 +1134,17 @@ async def test_kdr_world_given(self, mock_bot, mock_ctx, sample_matches_data): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=sample_matches_data) - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)"] - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.kdr.callback(cog, mock_ctx, world="Anvil Rock") mock_get_wid.assert_called_once_with(mock_ctx.bot, "Anvil Rock") @@ -1154,11 +1155,11 @@ async def test_kdr_wid_none(self, mock_bot, mock_ctx): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = None - with patch('src.gw2.cogs.wvw.Gw2Client'): - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.Gw2Client"): + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.kdr.callback(cog, mock_ctx, world="InvalidWorld") mock_error.assert_called_once() @@ -1172,17 +1173,17 @@ async def test_kdr_na_tier_title(self, mock_bot, mock_ctx, sample_matches_data): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=sample_matches_data) - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)"] - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.kdr.callback(cog, mock_ctx, world="Anvil Rock") embed = mock_send.call_args[0][1] @@ -1233,17 +1234,17 @@ async def test_kdr_eu_tier_title(self, mock_bot, mock_ctx): ], } - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 2001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=eu_matches) - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)"] - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.kdr.callback(cog, mock_ctx, world="Desolation") embed = mock_send.call_args[0][1] @@ -1256,15 +1257,15 @@ async def test_kdr_exception_during_fetch(self, mock_bot, mock_ctx): cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value error = Exception("API Error") mock_client_instance.call_api = AsyncMock(side_effect=error) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: await cog.kdr.callback(cog, mock_ctx, world="Anvil Rock") mock_error.assert_called_once_with(mock_ctx, error) @@ -1276,17 +1277,17 @@ async def test_kdr_successful_embed(self, mock_bot, mock_ctx, sample_matches_dat cog = GW2WvW(mock_bot) cog.bot = mock_ctx.bot - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(return_value=sample_matches_data) - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)"] - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: await cog.kdr.callback(cog, mock_ctx, world="Anvil Rock") mock_send.assert_called_once() @@ -1327,7 +1328,7 @@ def sample_matches(self): @pytest.mark.asyncio async def test_get_map_names_green(self, mock_ctx, sample_matches): """Test _get_map_names_embed_values for green color.""" - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)", "World2 (Medium)", "World3 (Low)"] result = await _get_map_names_embed_values(mock_ctx, "green", sample_matches) @@ -1341,7 +1342,7 @@ async def test_get_map_names_green(self, mock_ctx, sample_matches): @pytest.mark.asyncio async def test_get_map_names_primary_server_first(self, mock_ctx, sample_matches): """Test that primary server ID is first in the list.""" - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)"] await _get_map_names_embed_values(mock_ctx, "green", sample_matches) @@ -1359,7 +1360,7 @@ async def test_get_map_names_no_duplicates(self, mock_ctx): "worlds": {"red": 1001}, } - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_name_population', new_callable=AsyncMock) as mock_pop: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_name_population", new_callable=AsyncMock) as mock_pop: mock_pop.return_value = ["World1 (High)", "World2 (Medium)"] await _get_map_names_embed_values(mock_ctx, "red", matches) @@ -1787,7 +1788,7 @@ async def test_wvw_group_calls_invoke_subcommand(self, mock_bot, mock_ctx): """Test that wvw group command calls invoke_subcommand (line 27).""" cog = GW2WvW(mock_bot) - with patch('src.gw2.cogs.wvw.bot_utils.invoke_subcommand', new_callable=AsyncMock) as mock_invoke: + with patch("src.gw2.cogs.wvw.bot_utils.invoke_subcommand", new_callable=AsyncMock) as mock_invoke: await cog.wvw.callback(cog, mock_ctx) mock_invoke.assert_called_once_with(mock_ctx, "gw2 wvw") @@ -1846,15 +1847,15 @@ async def test_info_unknown_world_color_uses_default(self, mock_bot, mock_ctx): "maps": [{"objectives": [{"owner": "Yellow", "points_tick": 5}]}], } - with patch('src.gw2.cogs.wvw.gw2_utils.get_world_id', new_callable=AsyncMock) as mock_get_wid: + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock) as mock_get_wid: mock_get_wid.return_value = 1001 - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=[matches_data, sample_worldinfo]) - with patch('src.gw2.cogs.wvw.bot_utils.send_embed') as mock_send: - with patch('src.gw2.cogs.wvw.chat_formatting.inline', side_effect=lambda x: f"`{x}`"): + with patch("src.gw2.cogs.wvw.bot_utils.send_embed") as mock_send: + with patch("src.gw2.cogs.wvw.chat_formatting.inline", side_effect=lambda x: f"`{x}`"): await cog.info.callback(cog, mock_ctx, world="Anvil Rock") embed = mock_send.call_args[0][1] @@ -1901,22 +1902,21 @@ async def test_match_api_key_error_on_account_call(self, mock_bot, mock_ctx): api_key_data = [{"key": "test-api-key"}] - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_data) - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value # Raise APIKeyError on the account call (results = await gw2_api.call_api("account", api_key)) mock_client_instance.call_api = AsyncMock(side_effect=APIKeyError(mock_ctx.bot, "Invalid key")) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: result = await cog.match.callback(cog, mock_ctx, world=None) mock_error.assert_called_once() - from src.gw2.constants import gw2_messages - - assert mock_error.call_args[0][1] == gw2_messages.NO_API_KEY + error_msg = mock_error.call_args[0][1] + assert "Missing World Name" in error_msg class TestKdrAPIKeyError: @@ -1959,18 +1959,143 @@ async def test_kdr_api_key_error_on_account_call(self, mock_bot, mock_ctx): api_key_data = [{"key": "test-api-key"}] - with patch('src.gw2.cogs.wvw.Gw2KeyDal') as mock_dal: + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_api_key_by_user = AsyncMock(return_value=api_key_data) - with patch('src.gw2.cogs.wvw.Gw2Client') as mock_client: + with patch("src.gw2.cogs.wvw.Gw2Client") as mock_client: mock_client_instance = mock_client.return_value mock_client_instance.call_api = AsyncMock(side_effect=APIKeyError(mock_ctx.bot, "Invalid key")) - with patch('src.gw2.cogs.wvw.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: result = await cog.kdr.callback(cog, mock_ctx, world=None) mock_error.assert_called_once() - from src.gw2.constants import gw2_messages + error_msg = mock_error.call_args[0][1] + assert "Invalid world name" in error_msg + + +class TestResolveTier: + """Test cases for the _resolve_tier helper function.""" + + def test_na_tier(self): + """Test NA tier resolution from match ID.""" + matches = {"id": "1-3"} + result = _resolve_tier(matches) + assert "North America Tier" in result + assert "3" in result + + def test_eu_tier(self): + """Test EU tier resolution from match ID.""" + matches = {"id": "2-5"} + result = _resolve_tier(matches) + assert "Europe Tier" in result + assert "5" in result + + def test_na_tier_1(self): + """Test NA tier 1.""" + matches = {"id": "1-1"} + result = _resolve_tier(matches) + assert result == "North America Tier 1" + + def test_eu_tier_1(self): + """Test EU tier 1.""" + matches = {"id": "2-1"} + result = _resolve_tier(matches) + assert result == "Europe Tier 1" + + +class TestResolveWvwWorldId: + """Test cases for the _resolve_wvw_world_id method.""" + + @pytest.fixture + def mock_bot(self): + bot = MagicMock() + bot.db_session = MagicMock() + bot.log = MagicMock() + bot.settings = {"gw2": {"EmbedColor": 0x00FF00}} + return bot + + @pytest.fixture + def mock_ctx(self): + ctx = MagicMock() + ctx.bot = MagicMock() + ctx.bot.db_session = MagicMock() + ctx.bot.log = MagicMock() + ctx.message = MagicMock() + ctx.message.author = MagicMock() + ctx.message.author.id = 12345 + ctx.message.channel = MagicMock() + ctx.message.channel.typing = AsyncMock() + ctx.prefix = "!" + return ctx + + @pytest.mark.asyncio + async def test_with_world_name_delegates_to_get_world_id(self, mock_bot, mock_ctx): + """Test that providing a world name uses get_world_id.""" + cog = GW2WvW(mock_bot) + cog.bot = mock_ctx.bot + gw2_api = MagicMock() + + with patch("src.gw2.cogs.wvw.gw2_utils.get_world_id", new_callable=AsyncMock, return_value=1001): + result = await cog._resolve_wvw_world_id(mock_ctx, gw2_api, "Anvil Rock", "error msg") + assert result == 1001 + + @pytest.mark.asyncio + async def test_prefers_wvw_team_id_over_world(self, mock_bot, mock_ctx): + """Test that wvw.team_id is preferred over legacy world field.""" + cog = GW2WvW(mock_bot) + cog.bot = mock_ctx.bot + gw2_api = MagicMock() + gw2_api.call_api = AsyncMock(return_value={"world": 1001, "wvw": {"team_id": 11005}}) + + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: + mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=[{"key": "test-key"}]) + + result = await cog._resolve_wvw_world_id(mock_ctx, gw2_api, None, "error msg") + assert result == 11005 + + @pytest.mark.asyncio + async def test_falls_back_to_world_when_no_team_id(self, mock_bot, mock_ctx): + """Test fallback to world when wvw.team_id is absent.""" + cog = GW2WvW(mock_bot) + cog.bot = mock_ctx.bot + gw2_api = MagicMock() + gw2_api.call_api = AsyncMock(return_value={"world": 1001}) + + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: + mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=[{"key": "test-key"}]) + + result = await cog._resolve_wvw_world_id(mock_ctx, gw2_api, None, "error msg") + assert result == 1001 + + @pytest.mark.asyncio + async def test_no_api_key_sends_error(self, mock_bot, mock_ctx): + """Test that missing API key sends error and returns None.""" + cog = GW2WvW(mock_bot) + cog.bot = mock_ctx.bot + gw2_api = MagicMock() + + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: + mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=None) + + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: + result = await cog._resolve_wvw_world_id(mock_ctx, gw2_api, None, "no key msg") + assert result is None + mock_error.assert_called_once() + + @pytest.mark.asyncio + async def test_api_key_error_sends_error(self, mock_bot, mock_ctx): + """Test that APIKeyError sends error message.""" + cog = GW2WvW(mock_bot) + cog.bot = mock_ctx.bot + gw2_api = MagicMock() + gw2_api.call_api = AsyncMock(side_effect=APIKeyError(mock_ctx.bot, "bad key")) + + with patch("src.gw2.cogs.wvw.Gw2KeyDal") as mock_dal: + mock_dal.return_value.get_api_key_by_user = AsyncMock(return_value=[{"key": "test-key"}]) - assert mock_error.call_args[0][1] == gw2_messages.NO_API_KEY + with patch("src.gw2.cogs.wvw.bot_utils.send_error_msg") as mock_error: + result = await cog._resolve_wvw_world_id(mock_ctx, gw2_api, None, "error msg") + assert result is None + mock_error.assert_called_once() diff --git a/tests/unit/gw2/constants/test_gw2_settings.py b/tests/unit/gw2/constants/test_gw2_settings.py index 4cf3cec3..c33f7d12 100644 --- a/tests/unit/gw2/constants/test_gw2_settings.py +++ b/tests/unit/gw2/constants/test_gw2_settings.py @@ -162,15 +162,15 @@ def test_all_cooldown_fields_exist(self): settings = Gw2Settings() cooldown_fields = [ - 'account_cooldown', - 'api_keys_cooldown', - 'characters_cooldown', - 'config_cooldown', - 'daily_cooldown', - 'misc_cooldown', - 'session_cooldown', - 'worlds_cooldown', - 'wvw_cooldown', + "account_cooldown", + "api_keys_cooldown", + "characters_cooldown", + "config_cooldown", + "daily_cooldown", + "misc_cooldown", + "session_cooldown", + "worlds_cooldown", + "wvw_cooldown", ] for field in cooldown_fields: @@ -207,10 +207,10 @@ def test_settings_with_actual_env_file(self): settings = get_gw2_settings() # Just verify the structure is correct - assert hasattr(settings, 'embed_color') - assert hasattr(settings, 'api_version') - assert hasattr(settings, 'account_cooldown') - assert hasattr(settings, 'session_cooldown') + assert hasattr(settings, "embed_color") + assert hasattr(settings, "api_version") + assert hasattr(settings, "account_cooldown") + assert hasattr(settings, "session_cooldown") # Verify types assert isinstance(settings.embed_color, (str, type(None))) diff --git a/tests/unit/gw2/constants/test_gw2_teams.py b/tests/unit/gw2/constants/test_gw2_teams.py new file mode 100644 index 00000000..d853eb68 --- /dev/null +++ b/tests/unit/gw2/constants/test_gw2_teams.py @@ -0,0 +1,86 @@ +"""Tests for GW2 World Restructuring team constants.""" + +from src.gw2.constants.gw2_teams import WR_TEAM_NAMES, get_team_name, is_wr_team_id + + +class TestIsWrTeamId: + """Test cases for is_wr_team_id function.""" + + def test_na_team_id(self): + assert is_wr_team_id(11001) is True + + def test_na_team_id_last(self): + assert is_wr_team_id(11012) is True + + def test_eu_team_id(self): + assert is_wr_team_id(12001) is True + + def test_eu_team_id_last(self): + assert is_wr_team_id(12015) is True + + def test_legacy_na_world_id(self): + assert is_wr_team_id(1001) is False + + def test_legacy_eu_world_id(self): + assert is_wr_team_id(2001) is False + + def test_zero(self): + assert is_wr_team_id(0) is False + + def test_boundary_below(self): + assert is_wr_team_id(11000) is False + + def test_boundary_above(self): + assert is_wr_team_id(13000) is False + + def test_mid_range_gap(self): + """ID between NA and EU ranges is still considered WR.""" + assert is_wr_team_id(11500) is True + + +class TestGetTeamName: + """Test cases for get_team_name function.""" + + def test_na_team(self): + assert get_team_name(11001) == "Team 1 (NA)" + + def test_eu_team(self): + assert get_team_name(12001) == "Team 1 (EU)" + + def test_last_na_team(self): + assert get_team_name(11012) == "Team 12 (NA)" + + def test_last_eu_team(self): + assert get_team_name(12015) == "Team 15 (EU)" + + def test_unknown_team_id(self): + assert get_team_name(99999) is None + + def test_legacy_world_id(self): + assert get_team_name(1001) is None + + def test_zero(self): + assert get_team_name(0) is None + + +class TestWrTeamNames: + """Test cases for WR_TEAM_NAMES dict.""" + + def test_has_12_na_teams(self): + na_teams = {k: v for k, v in WR_TEAM_NAMES.items() if 11001 <= k <= 11999} + assert len(na_teams) == 12 + + def test_has_15_eu_teams(self): + eu_teams = {k: v for k, v in WR_TEAM_NAMES.items() if 12001 <= k <= 12999} + assert len(eu_teams) == 15 + + def test_total_teams(self): + assert len(WR_TEAM_NAMES) == 27 + + def test_all_na_names_contain_na(self): + for team_id in range(11001, 11013): + assert "(NA)" in WR_TEAM_NAMES[team_id] + + def test_all_eu_names_contain_eu(self): + for team_id in range(12001, 12016): + assert "(EU)" in WR_TEAM_NAMES[team_id] diff --git a/tests/unit/gw2/tools/test_gw2_client.py b/tests/unit/gw2/tools/test_gw2_client.py index d75f9589..78294dcd 100644 --- a/tests/unit/gw2/tools/test_gw2_client.py +++ b/tests/unit/gw2/tools/test_gw2_client.py @@ -296,7 +296,9 @@ async def test_call_api_with_key(self, gw2_client): headers = ( call_kwargs[1]["headers"] if "headers" in call_kwargs[1] - else call_kwargs[0][1] if len(call_kwargs[0]) > 1 else None + else call_kwargs[0][1] + if len(call_kwargs[0]) > 1 + else None ) # The headers parameter is passed as keyword arg if headers: @@ -870,7 +872,7 @@ async def test_call_api_non_200_206_returns_none_after_error_handler(self, gw2_c gw2_client.bot.aiosession.get = MagicMock(return_value=AsyncContextManager(mock_response)) # Patch _handle_api_error to NOT raise, so execution falls through to return None - with patch.object(gw2_client, '_handle_api_error', new_callable=AsyncMock) as mock_handler: + with patch.object(gw2_client, "_handle_api_error", new_callable=AsyncMock) as mock_handler: result = await gw2_client.call_api("account") mock_handler.assert_called_once() diff --git a/tests/unit/gw2/tools/test_gw2_cooldowns.py b/tests/unit/gw2/tools/test_gw2_cooldowns.py index 879f1efb..015f634c 100644 --- a/tests/unit/gw2/tools/test_gw2_cooldowns.py +++ b/tests/unit/gw2/tools/test_gw2_cooldowns.py @@ -9,7 +9,7 @@ class TestGW2CoolDowns: def test_cooldown_enum_values_exist(self): """Test that all expected cooldown values exist.""" - expected_cooldowns = ['Account', 'ApiKeys', 'Characters', 'Config', 'Daily', 'Misc', 'Session', 'Worlds', 'Wvw'] + expected_cooldowns = ["Account", "ApiKeys", "Characters", "Config", "Daily", "Misc", "Session", "Worlds", "Wvw"] for cooldown_name in expected_cooldowns: assert hasattr(GW2CoolDowns, cooldown_name) @@ -103,5 +103,5 @@ def test_cooldown_enum_iteration(self): assert isinstance(cooldown.value[0], int) # Should have all expected cooldowns - expected_names = {'Account', 'ApiKeys', 'Characters', 'Config', 'Daily', 'Misc', 'Session', 'Worlds', 'Wvw'} + expected_names = {"Account", "ApiKeys", "Characters", "Config", "Daily", "Misc", "Session", "Worlds", "Wvw"} assert cooldown_names == expected_names diff --git a/tests/unit/gw2/tools/test_gw2_utils.py b/tests/unit/gw2/tools/test_gw2_utils.py index c8d5dda6..e4274c2e 100644 --- a/tests/unit/gw2/tools/test_gw2_utils.py +++ b/tests/unit/gw2/tools/test_gw2_utils.py @@ -54,7 +54,7 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_send_msg_default_settings(self, mock_ctx): """Test send_msg with default settings.""" - with patch('src.gw2.tools.gw2_utils.bot_utils.send_embed') as mock_send: + with patch("src.gw2.tools.gw2_utils.bot_utils.send_embed") as mock_send: await send_msg(mock_ctx, "Test message") mock_send.assert_called_once() @@ -69,7 +69,7 @@ async def test_send_msg_default_settings(self, mock_ctx): @pytest.mark.asyncio async def test_send_msg_with_dm(self, mock_ctx): """Test send_msg with DM option.""" - with patch('src.gw2.tools.gw2_utils.bot_utils.send_embed') as mock_send: + with patch("src.gw2.tools.gw2_utils.bot_utils.send_embed") as mock_send: await send_msg(mock_ctx, "DM message", dm=True) mock_send.assert_called_once() @@ -99,7 +99,7 @@ def mock_server(self): @pytest.mark.asyncio async def test_insert_configs_when_not_exists(self, mock_bot, mock_server): """Test inserting configs when they don't exist.""" - with patch('src.gw2.tools.gw2_utils.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.tools.gw2_utils.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value=None) mock_instance.insert_gw2_server_configs = AsyncMock(return_value=None) @@ -112,7 +112,7 @@ async def test_insert_configs_when_not_exists(self, mock_bot, mock_server): @pytest.mark.asyncio async def test_skip_insert_when_configs_exist(self, mock_bot, mock_server): """Test skipping insert when configs already exist.""" - with patch('src.gw2.tools.gw2_utils.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.tools.gw2_utils.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value={"existing": "config"}) mock_instance.insert_gw2_server_configs = AsyncMock(return_value=None) @@ -282,14 +282,14 @@ def sample_account_data(self): @pytest.mark.asyncio async def test_calculate_achievement_points(self, mock_ctx, sample_user_achievements, sample_account_data): """Test calculating achievement points.""" - with patch('src.gw2.tools.gw2_utils.Gw2Client'): - with patch('src.gw2.tools.gw2_utils._fetch_achievement_data_in_batches') as mock_fetch: + with patch("src.gw2.tools.gw2_utils.Gw2Client"): + with patch("src.gw2.tools.gw2_utils._fetch_achievement_data_in_batches") as mock_fetch: mock_fetch.return_value = [ {"id": 1, "tiers": [{"count": 5, "points": 10}, {"count": 10, "points": 20}]}, {"id": 2, "tiers": [{"count": 3, "points": 5}]}, ] - with patch('src.gw2.tools.gw2_utils._calculate_earned_points') as mock_calc: + with patch("src.gw2.tools.gw2_utils._calculate_earned_points") as mock_calc: mock_calc.return_value = 75 result = await calculate_user_achiev_points(mock_ctx, sample_user_achievements, sample_account_data) @@ -416,7 +416,7 @@ def mock_bot(self): @pytest.mark.asyncio async def test_get_world_id_exact_match(self, mock_bot): """Test successful world ID retrieval with exact match.""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock( return_value=[{"id": 1001, "name": "Anvil Rock"}, {"id": 1002, "name": "Borlis Pass"}] @@ -428,7 +428,7 @@ async def test_get_world_id_exact_match(self, mock_bot): @pytest.mark.asyncio async def test_get_world_id_case_insensitive(self, mock_bot): """Test world ID retrieval with case-insensitive match.""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock( return_value=[ @@ -442,7 +442,7 @@ async def test_get_world_id_case_insensitive(self, mock_bot): @pytest.mark.asyncio async def test_get_world_id_partial_match(self, mock_bot): """Test world ID retrieval with partial match (line 196).""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock( return_value=[ @@ -459,7 +459,7 @@ async def test_get_world_id_partial_match(self, mock_bot): @pytest.mark.asyncio async def test_get_world_id_not_found(self, mock_bot): """Test world ID not found.""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(return_value=[{"id": 1001, "name": "Anvil Rock"}]) @@ -477,7 +477,7 @@ async def test_get_world_id_api_error(self, mock_bot): """Test get_world_id with API error.""" mock_bot.log.error = MagicMock() - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(side_effect=Exception("API Error")) @@ -499,24 +499,25 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_successful_retrieval(self, mock_ctx): - """Test successful world name population retrieval (lines 206-213).""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + """Test successful world name population retrieval for legacy IDs.""" + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock( return_value=[ - {"name": "Anvil Rock", "population": "High"}, - {"name": "Borlis Pass", "population": "Medium"}, + {"id": 1001, "name": "Anvil Rock", "population": "High"}, + {"id": 1002, "name": "Borlis Pass", "population": "Medium"}, ] ) result = await get_world_name_population(mock_ctx, "1001,1002") assert result == ["Anvil Rock", "Borlis Pass"] + mock_client.call_api.assert_called_once_with("worlds?ids=1001,1002") @pytest.mark.asyncio async def test_empty_results(self, mock_ctx): """Test when API returns empty results (line 211).""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(return_value=[]) @@ -527,7 +528,7 @@ async def test_empty_results(self, mock_ctx): @pytest.mark.asyncio async def test_none_results(self, mock_ctx): """Test when API returns None.""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(return_value=None) @@ -538,7 +539,7 @@ async def test_none_results(self, mock_ctx): @pytest.mark.asyncio async def test_exception_returns_none(self, mock_ctx): """Test that exception returns None (lines 215-217).""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(side_effect=Exception("API Error")) @@ -561,7 +562,7 @@ def mock_bot(self): @pytest.mark.asyncio async def test_successful_retrieval(self, mock_bot): """Test successful world name retrieval (lines 222-225).""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(return_value={"name": "Anvil Rock", "id": 1001}) @@ -572,7 +573,7 @@ async def test_successful_retrieval(self, mock_bot): @pytest.mark.asyncio async def test_no_result_returns_none(self, mock_bot): """Test that empty result returns None (line 225).""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(return_value=None) @@ -583,7 +584,7 @@ async def test_no_result_returns_none(self, mock_bot): @pytest.mark.asyncio async def test_result_without_name_key(self, mock_bot): """Test result dict without name key.""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(return_value={"id": 1001}) @@ -594,7 +595,7 @@ async def test_result_without_name_key(self, mock_bot): @pytest.mark.asyncio async def test_exception_returns_none(self, mock_bot): """Test that exception returns None (lines 227-229).""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(side_effect=Exception("API Error")) @@ -628,7 +629,7 @@ async def test_gw2_activity_detected_triggers_handler(self, mock_bot): before.activities = [] after.activities = [gw2_activity] - with patch('src.gw2.tools.gw2_utils._handle_gw2_activity_change') as mock_handle: + with patch("src.gw2.tools.gw2_utils._handle_gw2_activity_change") as mock_handle: mock_handle.return_value = None await check_gw2_game_activity(mock_bot, before, after) mock_handle.assert_called_once_with(mock_bot, after, gw2_activity) @@ -646,7 +647,7 @@ async def test_no_gw2_activity_does_nothing(self, mock_bot): before.activities = [] after.activities = [other_activity] - with patch('src.gw2.tools.gw2_utils._handle_gw2_activity_change') as mock_handle: + with patch("src.gw2.tools.gw2_utils._handle_gw2_activity_change") as mock_handle: await check_gw2_game_activity(mock_bot, before, after) mock_handle.assert_not_called() @@ -663,7 +664,7 @@ async def test_custom_activity_ignored(self, mock_bot): before.activities = [custom_activity] after.activities = [custom_activity] - with patch('src.gw2.tools.gw2_utils._handle_gw2_activity_change') as mock_handle: + with patch("src.gw2.tools.gw2_utils._handle_gw2_activity_change") as mock_handle: await check_gw2_game_activity(mock_bot, before, after) mock_handle.assert_not_called() @@ -691,7 +692,7 @@ def mock_member(self): @pytest.mark.asyncio async def test_no_server_configs_returns(self, mock_bot, mock_member): """Test that no server configs returns early (line 281).""" - with patch('src.gw2.tools.gw2_utils.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.tools.gw2_utils.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value=None) @@ -699,13 +700,13 @@ async def test_no_server_configs_returns(self, mock_bot, mock_member): await _handle_gw2_activity_change(mock_bot, mock_member, after_activity) # Should not proceed to Gw2KeyDal - with patch('src.gw2.tools.gw2_utils.Gw2KeyDal') as mock_key_dal: + with patch("src.gw2.tools.gw2_utils.Gw2KeyDal") as mock_key_dal: mock_key_dal.assert_not_called() @pytest.mark.asyncio async def test_session_not_active_returns(self, mock_bot, mock_member): """Test that inactive session returns early (line 281).""" - with patch('src.gw2.tools.gw2_utils.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.tools.gw2_utils.Gw2ConfigsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.get_gw2_server_configs = AsyncMock(return_value=[{"session": False}]) @@ -715,11 +716,11 @@ async def test_session_not_active_returns(self, mock_bot, mock_member): @pytest.mark.asyncio async def test_no_api_key_returns(self, mock_bot, mock_member): """Test that no API key returns early (lines 287-288).""" - with patch('src.gw2.tools.gw2_utils.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.tools.gw2_utils.Gw2ConfigsDal") as mock_dal: mock_configs = mock_dal.return_value mock_configs.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.tools.gw2_utils.Gw2KeyDal') as mock_key_dal: + with patch("src.gw2.tools.gw2_utils.Gw2KeyDal") as mock_key_dal: mock_key_instance = mock_key_dal.return_value mock_key_instance.get_api_key_by_user = AsyncMock(return_value=None) @@ -729,15 +730,15 @@ async def test_no_api_key_returns(self, mock_bot, mock_member): @pytest.mark.asyncio async def test_after_activity_not_none_starts_session(self, mock_bot, mock_member): """Test that non-None after_activity starts a session (lines 292-293).""" - with patch('src.gw2.tools.gw2_utils.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.tools.gw2_utils.Gw2ConfigsDal") as mock_dal: mock_configs = mock_dal.return_value mock_configs.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.tools.gw2_utils.Gw2KeyDal') as mock_key_dal: + with patch("src.gw2.tools.gw2_utils.Gw2KeyDal") as mock_key_dal: mock_key_instance = mock_key_dal.return_value mock_key_instance.get_api_key_by_user = AsyncMock(return_value=[{"key": "test-api-key-123"}]) - with patch('src.gw2.tools.gw2_utils.start_session') as mock_start: + with patch("src.gw2.tools.gw2_utils.start_session") as mock_start: mock_start.return_value = None after_activity = MagicMock() # Not None @@ -748,15 +749,15 @@ async def test_after_activity_not_none_starts_session(self, mock_bot, mock_membe @pytest.mark.asyncio async def test_after_activity_none_ends_session(self, mock_bot, mock_member): """Test that None after_activity ends a session (lines 294-295).""" - with patch('src.gw2.tools.gw2_utils.Gw2ConfigsDal') as mock_dal: + with patch("src.gw2.tools.gw2_utils.Gw2ConfigsDal") as mock_dal: mock_configs = mock_dal.return_value mock_configs.get_gw2_server_configs = AsyncMock(return_value=[{"session": True}]) - with patch('src.gw2.tools.gw2_utils.Gw2KeyDal') as mock_key_dal: + with patch("src.gw2.tools.gw2_utils.Gw2KeyDal") as mock_key_dal: mock_key_instance = mock_key_dal.return_value mock_key_instance.get_api_key_by_user = AsyncMock(return_value=[{"key": "test-api-key-123"}]) - with patch('src.gw2.tools.gw2_utils.end_session') as mock_end: + with patch("src.gw2.tools.gw2_utils.end_session") as mock_end: mock_end.return_value = None await _handle_gw2_activity_change(mock_bot, mock_member, None) @@ -785,13 +786,13 @@ def mock_member(self): @pytest.mark.asyncio async def test_get_user_stats_returns_none(self, mock_bot, mock_member): """Test that None user stats returns early (lines 300-302).""" - with patch('src.gw2.tools.gw2_utils.get_user_stats') as mock_stats: + with patch("src.gw2.tools.gw2_utils.get_user_stats") as mock_stats: mock_stats.return_value = None await start_session(mock_bot, mock_member, "api-key") # Should not proceed to session dal - with patch('src.gw2.tools.gw2_utils.Gw2SessionsDal') as mock_dal: + with patch("src.gw2.tools.gw2_utils.Gw2SessionsDal") as mock_dal: mock_dal.assert_not_called() @pytest.mark.asyncio @@ -799,20 +800,20 @@ async def test_successful_start_session(self, mock_bot, mock_member): """Test successful session start (lines 304-309).""" session_data = {"acc_name": "TestUser.1234", "wvw_rank": 50, "gold": 1000} - with patch('src.gw2.tools.gw2_utils.get_user_stats') as mock_stats: + with patch("src.gw2.tools.gw2_utils.get_user_stats") as mock_stats: mock_stats.return_value = session_data.copy() - with patch('src.gw2.tools.gw2_utils.bot_utils.convert_datetime_to_str_short') as mock_convert: + with patch("src.gw2.tools.gw2_utils.bot_utils.convert_datetime_to_str_short") as mock_convert: mock_convert.return_value = "2023-01-01" - with patch('src.gw2.tools.gw2_utils.bot_utils.get_current_date_time') as mock_time: + with patch("src.gw2.tools.gw2_utils.bot_utils.get_current_date_time") as mock_time: mock_time.return_value = datetime(2023, 1, 1, 12, 0, 0) - with patch('src.gw2.tools.gw2_utils.Gw2SessionsDal') as mock_session_dal: + with patch("src.gw2.tools.gw2_utils.Gw2SessionsDal") as mock_session_dal: mock_instance = mock_session_dal.return_value mock_instance.insert_start_session = AsyncMock(return_value=42) - with patch('src.gw2.tools.gw2_utils.insert_session_char') as mock_insert_char: + with patch("src.gw2.tools.gw2_utils.insert_session_char") as mock_insert_char: mock_insert_char.return_value = None await start_session(mock_bot, mock_member, "api-key") @@ -846,7 +847,7 @@ def mock_member(self): @pytest.mark.asyncio async def test_get_user_stats_returns_none(self, mock_bot, mock_member): """Test that None user stats returns early (lines 314-316).""" - with patch('src.gw2.tools.gw2_utils.get_user_stats') as mock_stats: + with patch("src.gw2.tools.gw2_utils.get_user_stats") as mock_stats: mock_stats.return_value = None await end_session(mock_bot, mock_member, "api-key") @@ -856,20 +857,20 @@ async def test_successful_end_session(self, mock_bot, mock_member): """Test successful session end (lines 318-323).""" session_data = {"acc_name": "TestUser.1234", "wvw_rank": 50, "gold": 1000} - with patch('src.gw2.tools.gw2_utils.get_user_stats') as mock_stats: + with patch("src.gw2.tools.gw2_utils.get_user_stats") as mock_stats: mock_stats.return_value = session_data.copy() - with patch('src.gw2.tools.gw2_utils.bot_utils.convert_datetime_to_str_short') as mock_convert: + with patch("src.gw2.tools.gw2_utils.bot_utils.convert_datetime_to_str_short") as mock_convert: mock_convert.return_value = "2023-01-01" - with patch('src.gw2.tools.gw2_utils.bot_utils.get_current_date_time') as mock_time: + with patch("src.gw2.tools.gw2_utils.bot_utils.get_current_date_time") as mock_time: mock_time.return_value = datetime(2023, 1, 1, 12, 0, 0) - with patch('src.gw2.tools.gw2_utils.Gw2SessionsDal') as mock_session_dal: + with patch("src.gw2.tools.gw2_utils.Gw2SessionsDal") as mock_session_dal: mock_instance = mock_session_dal.return_value mock_instance.update_end_session = AsyncMock(return_value=42) - with patch('src.gw2.tools.gw2_utils.insert_session_char') as mock_insert_char: + with patch("src.gw2.tools.gw2_utils.insert_session_char") as mock_insert_char: mock_insert_char.return_value = None await end_session(mock_bot, mock_member, "api-key") @@ -896,7 +897,7 @@ def mock_bot(self): @pytest.mark.asyncio async def test_api_exception_returns_none(self, mock_bot): """Test that API exception returns None (lines 336-338).""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(side_effect=Exception("API Error")) @@ -907,7 +908,7 @@ async def test_api_exception_returns_none(self, mock_bot): @pytest.mark.asyncio async def test_successful_stats_retrieval(self, mock_bot): - """Test successful user stats retrieval (lines 328-344).""" + """Test successful user stats retrieval with legacy wvw_rank.""" account_data = {"name": "TestUser.1234", "wvw_rank": 50} wallet_data = [ {"id": 1, "value": 50000}, # gold @@ -919,7 +920,7 @@ async def test_successful_stats_retrieval(self, mock_bot): {"id": 291, "current": 42}, # camps ] - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(side_effect=[account_data, wallet_data, achievements_data]) @@ -934,6 +935,22 @@ async def test_successful_stats_retrieval(self, mock_bot): assert result["players"] == 150 assert result["camps"] == 42 + @pytest.mark.asyncio + async def test_successful_stats_retrieval_new_wvw_format(self, mock_bot): + """Test user stats retrieval with new wvw.rank format.""" + account_data = {"name": "TestUser.1234", "wvw": {"rank": 200}} + wallet_data = [] + achievements_data = [] + + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: + mock_client = mock_client_class.return_value + mock_client.call_api = AsyncMock(side_effect=[account_data, wallet_data, achievements_data]) + + result = await get_user_stats(mock_bot, "api-key") + + assert result is not None + assert result["wvw_rank"] == 200 + @pytest.mark.asyncio async def test_stats_with_all_wallet_items(self, mock_bot): """Test stats with all wallet items populated.""" @@ -950,7 +967,7 @@ async def test_stats_with_all_wallet_items(self, mock_bot): ] achievements_data = [] - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(side_effect=[account_data, wallet_data, achievements_data]) @@ -1155,12 +1172,12 @@ def mock_member(self): @pytest.mark.asyncio async def test_successful_insert(self, mock_bot, mock_member): """Test successful session character insert (lines 415-428).""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value characters_data = [{"name": "CharName", "level": 80}] mock_client.call_api = AsyncMock(return_value=characters_data) - with patch('src.gw2.tools.gw2_utils.Gw2SessionCharsDal') as mock_dal: + with patch("src.gw2.tools.gw2_utils.Gw2SessionCharsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.insert_session_char = AsyncMock() @@ -1182,11 +1199,11 @@ async def test_successful_insert(self, mock_bot, mock_member): @pytest.mark.asyncio async def test_insert_end_session_type(self, mock_bot, mock_member): """Test insert with end session type.""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(return_value=[]) - with patch('src.gw2.tools.gw2_utils.Gw2SessionCharsDal') as mock_dal: + with patch("src.gw2.tools.gw2_utils.Gw2SessionCharsDal") as mock_dal: mock_instance = mock_dal.return_value mock_instance.insert_session_char = AsyncMock() @@ -1200,7 +1217,7 @@ async def test_insert_end_session_type(self, mock_bot, mock_member): @pytest.mark.asyncio async def test_exception_logs_error(self, mock_bot, mock_member): """Test that exception is caught and logged (lines 430-431).""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(side_effect=Exception("API Error")) @@ -1510,7 +1527,7 @@ def mock_ctx_dm(self): @pytest.mark.asyncio async def test_delete_api_key_in_guild(self, mock_ctx_guild): """Test deleting API key message in guild.""" - with patch('src.gw2.tools.gw2_utils.send_msg') as mock_send: + with patch("src.gw2.tools.gw2_utils.send_msg") as mock_send: await delete_api_key(mock_ctx_guild, message=True) mock_ctx_guild.message.delete.assert_called_once() @@ -1528,14 +1545,14 @@ async def test_delete_api_key_http_exception(self, mock_ctx_guild): side_effect=discord.HTTPException(response=MagicMock(), message="Forbidden") ) - with patch('src.gw2.tools.gw2_utils.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.tools.gw2_utils.bot_utils.send_error_msg") as mock_error: await delete_api_key(mock_ctx_guild, message=True) mock_error.assert_called_once() @pytest.mark.asyncio async def test_delete_api_key_no_message_flag(self, mock_ctx_guild): """Test delete without message flag.""" - with patch('src.gw2.tools.gw2_utils.send_msg') as mock_send: + with patch("src.gw2.tools.gw2_utils.send_msg") as mock_send: await delete_api_key(mock_ctx_guild, message=False) mock_ctx_guild.message.delete.assert_called_once() @@ -1581,7 +1598,7 @@ def mock_ctx(self): @pytest.mark.asyncio async def test_get_worlds_ids_success(self, mock_ctx): """Test successful world IDs retrieval.""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(return_value=[{"id": 1001, "name": "Anvil Rock"}]) @@ -1594,11 +1611,11 @@ async def test_get_worlds_ids_success(self, mock_ctx): @pytest.mark.asyncio async def test_get_worlds_ids_api_error(self, mock_ctx): """Test get_worlds_ids with API error.""" - with patch('src.gw2.tools.gw2_utils.Gw2Client') as mock_client_class: + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: mock_client = mock_client_class.return_value mock_client.call_api = AsyncMock(side_effect=APIConnectionError(mock_ctx.bot, "API Error")) - with patch('src.gw2.tools.gw2_utils.bot_utils.send_error_msg') as mock_error: + with patch("src.gw2.tools.gw2_utils.bot_utils.send_error_msg") as mock_error: success, results = await get_worlds_ids(mock_ctx) assert success is False @@ -1679,7 +1696,7 @@ class TestCreateInitialUserStats: """Test cases for _create_initial_user_stats function.""" def test_creates_correct_structure(self): - """Test that initial stats structure is correct.""" + """Test that initial stats structure is correct with legacy wvw_rank.""" account_data = {"name": "TestUser.1234", "wvw_rank": 75} result = _create_initial_user_stats(account_data) @@ -1701,3 +1718,80 @@ def test_creates_correct_structure(self): assert result["castles"] == 0 assert result["towers"] == 0 assert result["keeps"] == 0 + + def test_new_wvw_rank_format(self): + """Test that wvw.rank (new API format) is preferred over wvw_rank.""" + account_data = {"name": "TestUser.1234", "wvw": {"rank": 200}, "wvw_rank": 75} + + result = _create_initial_user_stats(account_data) + + assert result["wvw_rank"] == 200 + + def test_fallback_to_legacy_wvw_rank(self): + """Test fallback to wvw_rank when wvw.rank is absent.""" + account_data = {"name": "TestUser.1234", "wvw_rank": 75} + + result = _create_initial_user_stats(account_data) + + assert result["wvw_rank"] == 75 + + def test_no_wvw_rank_defaults_to_zero(self): + """Test that missing both wvw.rank and wvw_rank defaults to 0.""" + account_data = {"name": "TestUser.1234"} + + result = _create_initial_user_stats(account_data) + + assert result["wvw_rank"] == 0 + + def test_wvw_rank_zero_in_new_format_falls_back(self): + """Test that wvw.rank=0 (falsy) falls back to wvw_rank.""" + account_data = {"name": "TestUser.1234", "wvw": {"rank": 0}, "wvw_rank": 50} + + result = _create_initial_user_stats(account_data) + + # 0 is falsy, so it falls back to wvw_rank + assert result["wvw_rank"] == 50 + + +class TestGetWorldNamePopulationWithWR: + """Test cases for get_world_name_population with WR team IDs.""" + + @pytest.fixture + def mock_ctx(self): + """Create a mock command context.""" + ctx = MagicMock() + ctx.bot = MagicMock() + ctx.bot.log = MagicMock() + return ctx + + @pytest.mark.asyncio + async def test_wr_team_ids_only(self, mock_ctx): + """Test resolving only WR team IDs (no API call needed).""" + result = await get_world_name_population(mock_ctx, "11001,12001") + + assert result is not None + assert len(result) == 2 + assert "Team 1 (NA)" in result + assert "Team 1 (EU)" in result + + @pytest.mark.asyncio + async def test_mixed_legacy_and_wr_ids(self, mock_ctx): + """Test resolving a mix of legacy world IDs and WR team IDs.""" + with patch("src.gw2.tools.gw2_utils.Gw2Client") as mock_client_class: + mock_client = mock_client_class.return_value + mock_client.call_api = AsyncMock(return_value=[{"id": 1001, "name": "Anvil Rock", "population": "High"}]) + + result = await get_world_name_population(mock_ctx, "1001,11005") + + assert result is not None + assert len(result) == 2 + assert result[0] == "Anvil Rock" + assert result[1] == "Team 5 (NA)" + + @pytest.mark.asyncio + async def test_unknown_wr_team_id(self, mock_ctx): + """Test resolving an unknown WR team ID uses fallback name.""" + result = await get_world_name_population(mock_ctx, "11999") + + assert result is not None + assert result[0] == "Team 11999" diff --git a/uv.lock b/uv.lock index d1255877..06fc7d4c 100644 --- a/uv.lock +++ b/uv.lock @@ -204,28 +204,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/dd/0b074d89e903cc771721cde2c4bf3d8c9d114b5bd791af5c62bcf5fb9459/better_profanity-0.7.0-py3-none-any.whl", hash = "sha256:bd4c529ea6aa2db1aaa50524be1ed14d0fe5c664f1fd88c8bc388c7e9f9f00e8", size = 46104, upload-time = "2020-11-02T10:49:56.066Z" }, ] -[[package]] -name = "black" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, -] - [[package]] name = "certifi" version = "2026.1.4" @@ -387,7 +365,7 @@ wheels = [ [[package]] name = "discordbot" -version = "3.0.1" +version = "3.0.2" source = { virtual = "." } dependencies = [ { name = "alembic" }, @@ -403,7 +381,6 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "black" }, { name = "poethepoet" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -419,18 +396,17 @@ requires-dist = [ { name = "ddcdatabases", extras = ["postgres"], specifier = ">=3.0.10" }, { name = "discord-py", specifier = ">=2.6.4" }, { name = "gtts", specifier = ">=2.5.4" }, - { name = "openai", specifier = ">=2.20.0" }, + { name = "openai", specifier = ">=2.21.0" }, { name = "pynacl", specifier = ">=1.6.2" }, { name = "pythonlogs", specifier = ">=6.0.2" }, ] [package.metadata.requires-dev] dev = [ - { name = "black", specifier = ">=26.1.0" }, { name = "poethepoet", specifier = ">=0.41.0" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, - { name = "ruff", specifier = ">=0.15.0" }, + { name = "ruff", specifier = ">=0.15.1" }, { name = "testcontainers", extras = ["postgres"], specifier = ">=4.14.1" }, ] @@ -711,18 +687,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - [[package]] name = "openai" -version = "2.20.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -734,9 +701,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/5a/f495777c02625bfa18212b6e3b73f1893094f2bf660976eb4bc6f43a1ca2/openai-2.20.0.tar.gz", hash = "sha256:2654a689208cd0bf1098bb9462e8d722af5cbe961e6bba54e6f19fb843d88db1", size = 642355, upload-time = "2026-02-10T19:02:54.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/e5/3d197a0947a166649f566706d7a4c8f7fe38f1fa7b24c9bcffe4c7591d44/openai-2.21.0.tar.gz", hash = "sha256:81b48ce4b8bbb2cc3af02047ceb19561f7b1dc0d4e52d1de7f02abfd15aa59b7", size = 644374, upload-time = "2026-02-14T00:12:01.577Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/a0/cf4297aa51bbc21e83ef0ac018947fa06aea8f2364aad7c96cbf148590e6/openai-2.20.0-py3-none-any.whl", hash = "sha256:38d989c4b1075cd1f76abc68364059d822327cf1a932531d429795f4fc18be99", size = 1098479, upload-time = "2026-02-10T19:02:52.157Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/0a89092a453bb2c676d66abee44f863e742b2110d4dbb1dbcca3f7e5fc33/openai-2.21.0-py3-none-any.whl", hash = "sha256:0bc1c775e5b1536c294eded39ee08f8407656537ccc71b1004104fe1602e267c", size = 1103065, upload-time = "2026-02-14T00:11:59.603Z" }, ] [[package]] @@ -757,24 +724,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, ] -[[package]] -name = "pathspec" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/474d0a8508029286b905622e6929470fb84337cfa08f9d09fbb624515249/platformdirs-4.6.0.tar.gz", hash = "sha256:4a13c2db1071e5846c3b3e04e5b095c0de36b2a24be9a3bc0145ca66fce4e328", size = 23433, upload-time = "2026-02-12T14:36:21.288Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/10/1b0dcf51427326f70e50d98df21b18c228117a743a1fc515a42f8dc7d342/platformdirs-4.6.0-py3-none-any.whl", hash = "sha256:dd7f808d828e1764a22ebff09e60f175ee3c41876606a6132a688d809c7c9c73", size = 19549, upload-time = "2026-02-12T14:36:19.743Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -936,16 +885,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/a1/ae859ffac5a3338a66b74c5e29e244fd3a3cc483c89feaf9f56c39898d75/pydantic_settings-2.13.0.tar.gz", hash = "sha256:95d875514610e8595672800a5c40b073e99e4aae467fa7c8f9c263061ea2e1fe", size = 222450, upload-time = "2026-02-15T12:11:23.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1a/dd1b9d7e627486cf8e7523d09b70010e05a4bc41414f4ae6ce184cf0afb6/pydantic_settings-2.13.0-py3-none-any.whl", hash = "sha256:d67b576fff39cd086b595441bf9c75d4193ca9c0ed643b90360694d0f1240246", size = 58429, upload-time = "2026-02-15T12:11:22.133Z" }, ] [[package]] @@ -1055,25 +1004,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/60/fdafb69e7f6b99455c1f74d835484844d297a9f1027cbb6a76a11f5ef9b7/pythonlogs-6.0.2-py3-none-any.whl", hash = "sha256:ff227f60bdc7aa091ade76e14f41b3b706f3ef1e2260373e5e50f610fda773c4", size = 25299, upload-time = "2026-02-09T17:24:32.649Z" }, ] -[[package]] -name = "pytokens" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, - { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, - { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, - { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, - { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, - { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, - { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, - { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, -] - [[package]] name = "pywin32" version = "311" @@ -1127,27 +1057,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, - { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, - { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, - { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, - { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, - { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, - { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] [[package]]