aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/README.md12
-rw-r--r--tests/base.py8
-rw-r--r--tests/bot/exts/backend/sync/test_cog.py6
-rw-r--r--tests/bot/exts/backend/sync/test_roles.py6
-rw-r--r--tests/bot/exts/backend/sync/test_users.py2
-rw-r--r--tests/bot/exts/backend/test_error_handler.py64
-rw-r--r--tests/bot/exts/events/test_code_jams.py4
-rw-r--r--tests/bot/exts/filters/test_antimalware.py2
-rw-r--r--tests/bot/exts/filters/test_security.py2
-rw-r--r--tests/bot/exts/filters/test_token_remover.py2
-rw-r--r--tests/bot/exts/info/test_information.py98
-rw-r--r--tests/bot/exts/moderation/infraction/test_infractions.py177
-rw-r--r--tests/bot/exts/moderation/infraction/test_utils.py38
-rw-r--r--tests/bot/exts/moderation/test_clean.py104
-rw-r--r--tests/bot/exts/moderation/test_incidents.py20
-rw-r--r--tests/bot/exts/moderation/test_modlog.py4
-rw-r--r--tests/bot/exts/moderation/test_silence.py4
-rw-r--r--tests/bot/exts/test_cogs.py4
-rw-r--r--tests/bot/exts/utils/test_snekbox.py26
-rw-r--r--tests/bot/test_converters.py19
-rw-r--r--tests/bot/utils/test_checks.py2
-rw-r--r--tests/bot/utils/test_time.py47
-rw-r--r--tests/helpers.py124
-rw-r--r--tests/test_helpers.py28
24 files changed, 490 insertions, 313 deletions
diff --git a/tests/README.md b/tests/README.md
index b7fddfaa2..fc03b3d43 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -121,9 +121,9 @@ As we are trying to test our "units" of code independently, we want to make sure
However, the features that we are trying to test often depend on those objects generated by external pieces of code. It would be difficult to test a bot command without having access to a `Context` instance. Fortunately, there's a solution for that: we use fake objects that act like the true object. We call these fake objects "mocks".
-To create these mock object, we mainly use the [`unittest.mock`](https://docs.python.org/3/library/unittest.mock.html) module. In addition, we have also defined a couple of specialized mock objects that mock specific `discord.py` types (see the section on the below.).
+To create these mock object, we mainly use the [`unittest.mock`](https://docs.python.org/3/library/unittest.mock.html) module. In addition, we have also defined a couple of specialized mock objects that mock specific `disnake` types (see the section on the below.).
-An example of mocking is when we provide a command with a mocked version of `discord.ext.commands.Context` object instead of a real `Context` object. This makes sure we can then check (_assert_) if the `send` method of the mocked Context object was called with the correct message content (without having to send a real message to the Discord API!):
+An example of mocking is when we provide a command with a mocked version of `disnake.ext.commands.Context` object instead of a real `Context` object. This makes sure we can then check (_assert_) if the `send` method of the mocked Context object was called with the correct message content (without having to send a real message to the Discord API!):
```py
import asyncio
@@ -152,15 +152,15 @@ class BotCogTests(unittest.TestCase):
By default, the `unittest.mock.Mock` and `unittest.mock.MagicMock` classes cannot mock coroutines, since the `__call__` method they provide is synchronous. The [`AsyncMock`](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.AsyncMock) that has been [introduced in Python 3.8](https://docs.python.org/3.9/whatsnew/3.8.html#unittest) is an asynchronous version of `MagicMock` that can be used anywhere a coroutine is expected.
-### Special mocks for some `discord.py` types
+### Special mocks for some `disnake` types
To quote Ned Batchelder, Mock objects are "automatic chameleons". This means that they will happily allow the access to any attribute or method and provide a mocked value in return. One downside to this is that if the code you are testing gets the name of the attribute wrong, your mock object will not complain and the test may still pass.
-In order to avoid that, we have defined a number of Mock types in [`helpers.py`](/tests/helpers.py) that follow the specifications of the actual Discord types they are mocking. This means that trying to access an attribute or method on a mocked object that does not exist on the equivalent `discord.py` object will result in an `AttributeError`. In addition, these mocks have some sensible defaults and **pass `isinstance` checks for the types they are mocking**.
+In order to avoid that, we have defined a number of Mock types in [`helpers.py`](/tests/helpers.py) that follow the specifications of the actual disnake types they are mocking. This means that trying to access an attribute or method on a mocked object that does not exist on the equivalent `disnake` object will result in an `AttributeError`. In addition, these mocks have some sensible defaults and **pass `isinstance` checks for the types they are mocking**.
These special mocks are added when they are needed, so if you think it would be sensible to add another one, feel free to propose one in your PR.
-**Note:** These mock types only "know" the attributes that are set by default when these `discord.py` types are first initialized. If you need to work with dynamically set attributes that are added after initialization, you can still explicitly mock them:
+**Note:** These mock types only "know" the attributes that are set by default when these `disnake` types are first initialized. If you need to work with dynamically set attributes that are added after initialization, you can still explicitly mock them:
```py
import unittest.mock
@@ -245,7 +245,7 @@ All in all, it's not only important to consider if all statements or branches we
### Unit Testing vs Integration Testing
-Another restriction of unit testing is that it tests, well, in units. Even if we can guarantee that the units work as they should independently, we have no guarantee that they will actually work well together. Even more, while the mocking described above gives us a lot of flexibility in factoring out external code, we are work under the implicit assumption that we fully understand those external parts and utilize it correctly. What if our mocked `Context` object works with a `send` method, but `discord.py` has changed it to a `send_message` method in a recent update? It could mean our tests are passing, but the code it's testing still doesn't work in production.
+Another restriction of unit testing is that it tests, well, in units. Even if we can guarantee that the units work as they should independently, we have no guarantee that they will actually work well together. Even more, while the mocking described above gives us a lot of flexibility in factoring out external code, we are work under the implicit assumption that we fully understand those external parts and utilize it correctly. What if our mocked `Context` object works with a `send` method, but `disnake` has changed it to a `send_message` method in a recent update? It could mean our tests are passing, but the code it's testing still doesn't work in production.
The answer to this is that we also need to make sure that the individual parts come together into a working application. In addition, we will also need to make sure that the application communicates correctly with external applications. Since we currently have no automated integration tests or functional tests, that means **it's still very important to fire up the bot and test the code you've written manually** in addition to the unit tests you've written.
diff --git a/tests/base.py b/tests/base.py
index 5e304ea9d..dea7dd678 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -3,8 +3,8 @@ import unittest
from contextlib import contextmanager
from typing import Dict
-import discord
-from discord.ext import commands
+import disnake
+from disnake.ext import commands
from bot.log import get_logger
from tests import helpers
@@ -80,7 +80,7 @@ class LoggingTestsMixin:
class CommandTestCase(unittest.IsolatedAsyncioTestCase):
- """TestCase with additional assertions that are useful for testing Discord commands."""
+ """TestCase with additional assertions that are useful for testing disnake commands."""
async def assertHasPermissionsCheck( # noqa: N802
self,
@@ -98,7 +98,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):
permissions = {k: not v for k, v in permissions.items()}
ctx = helpers.MockContext()
- ctx.channel.permissions_for.return_value = discord.Permissions(**permissions)
+ ctx.channel.permissions_for.return_value = disnake.Permissions(**permissions)
with self.assertRaises(commands.MissingPermissions) as cm:
await cmd.can_run(ctx)
diff --git a/tests/bot/exts/backend/sync/test_cog.py b/tests/bot/exts/backend/sync/test_cog.py
index fdd0ab74a..4ed7de64d 100644
--- a/tests/bot/exts/backend/sync/test_cog.py
+++ b/tests/bot/exts/backend/sync/test_cog.py
@@ -1,7 +1,7 @@
import unittest
from unittest import mock
-import discord
+import disnake
from bot import constants
from bot.api import ResponseCodeError
@@ -257,9 +257,9 @@ class SyncCogListenerTests(SyncCogTestCase):
self.assertTrue(self.cog.on_member_update.__cog_listener__)
subtests = (
- ("activities", discord.Game("Pong"), discord.Game("Frogger")),
+ ("activities", disnake.Game("Pong"), disnake.Game("Frogger")),
("nick", "old nick", "new nick"),
- ("status", discord.Status.online, discord.Status.offline),
+ ("status", disnake.Status.online, disnake.Status.offline),
)
for attribute, old_value, new_value in subtests:
diff --git a/tests/bot/exts/backend/sync/test_roles.py b/tests/bot/exts/backend/sync/test_roles.py
index 541074336..9ecb8fae0 100644
--- a/tests/bot/exts/backend/sync/test_roles.py
+++ b/tests/bot/exts/backend/sync/test_roles.py
@@ -1,7 +1,7 @@
import unittest
from unittest import mock
-import discord
+import disnake
from bot.exts.backend.sync._syncers import RoleSyncer, _Diff, _Role
from tests import helpers
@@ -34,8 +34,8 @@ class RoleSyncerDiffTests(unittest.IsolatedAsyncioTestCase):
for role in roles:
mock_role = helpers.MockRole(**role)
- mock_role.colour = discord.Colour(role["colour"])
- mock_role.permissions = discord.Permissions(role["permissions"])
+ mock_role.colour = disnake.Colour(role["colour"])
+ mock_role.permissions = disnake.Permissions(role["permissions"])
guild.roles.append(mock_role)
return guild
diff --git a/tests/bot/exts/backend/sync/test_users.py b/tests/bot/exts/backend/sync/test_users.py
index 2fc97af2d..f55f5360f 100644
--- a/tests/bot/exts/backend/sync/test_users.py
+++ b/tests/bot/exts/backend/sync/test_users.py
@@ -1,7 +1,7 @@
import unittest
from unittest import mock
-from discord.errors import NotFound
+from disnake.errors import NotFound
from bot.exts.backend.sync._syncers import UserSyncer, _Diff
from tests import helpers
diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py
index 462f718e6..83b5f2749 100644
--- a/tests/bot/exts/backend/test_error_handler.py
+++ b/tests/bot/exts/backend/test_error_handler.py
@@ -1,7 +1,7 @@
import unittest
from unittest.mock import AsyncMock, MagicMock, call, patch
-from discord.ext.commands import errors
+from disnake.ext.commands import errors
from bot.api import ResponseCodeError
from bot.errors import InvalidInfractedUserError, LockedResourceError
@@ -337,14 +337,12 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase):
async def test_try_get_tag_get_command(self):
"""Should call `Bot.get_command` with `tags get` argument."""
self.bot.get_command.reset_mock()
- self.ctx.invoked_with = "foo"
await self.cog.try_get_tag(self.ctx)
self.bot.get_command.assert_called_once_with("tags get")
async def test_try_get_tag_invoked_from_error_handler(self):
"""`self.ctx` should have `invoked_from_error_handler` `True`."""
self.ctx.invoked_from_error_handler = False
- self.ctx.invoked_with = "foo"
await self.cog.try_get_tag(self.ctx)
self.assertTrue(self.ctx.invoked_from_error_handler)
@@ -359,38 +357,12 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase):
err = errors.CommandError()
self.tag.get_command.can_run = AsyncMock(side_effect=err)
self.cog.on_command_error = AsyncMock()
- self.ctx.invoked_with = "foo"
self.assertIsNone(await self.cog.try_get_tag(self.ctx))
self.cog.on_command_error.assert_awaited_once_with(self.ctx, err)
- @patch("bot.exts.backend.error_handler.TagNameConverter")
- async def test_try_get_tag_convert_success(self, tag_converter):
- """Converting tag should successful."""
- self.ctx.invoked_with = "foo"
- tag_converter.convert = AsyncMock(return_value="foo")
- self.assertIsNone(await self.cog.try_get_tag(self.ctx))
- tag_converter.convert.assert_awaited_once_with(self.ctx, "foo")
- self.ctx.invoke.assert_awaited_once()
-
- @patch("bot.exts.backend.error_handler.TagNameConverter")
- async def test_try_get_tag_convert_fail(self, tag_converter):
- """Converting tag should raise `BadArgument`."""
- self.ctx.reset_mock()
- self.ctx.invoked_with = "bar"
- tag_converter.convert = AsyncMock(side_effect=errors.BadArgument())
- self.assertIsNone(await self.cog.try_get_tag(self.ctx))
- self.ctx.invoke.assert_not_awaited()
-
- async def test_try_get_tag_ctx_invoke(self):
- """Should call `ctx.invoke` with proper args/kwargs."""
- self.ctx.reset_mock()
- self.ctx.invoked_with = "foo"
- self.assertIsNone(await self.cog.try_get_tag(self.ctx))
- self.ctx.invoke.assert_awaited_once_with(self.tag.get_command, tag_name="foo")
-
async def test_dont_call_suggestion_tag_sent(self):
"""Should never call command suggestion if tag is already sent."""
- self.ctx.invoked_with = "foo"
+ self.ctx.message = MagicMock(content="foo")
self.ctx.invoke = AsyncMock(return_value=True)
self.cog.send_command_suggestion = AsyncMock()
@@ -572,38 +544,6 @@ class IndividualErrorHandlerTests(unittest.IsolatedAsyncioTestCase):
push_scope_mock.set_extra.has_calls(set_extra_calls)
-class OtherErrorHandlerTests(unittest.IsolatedAsyncioTestCase):
- """Other `ErrorHandler` tests."""
-
- def setUp(self):
- self.bot = MockBot()
- self.ctx = MockContext()
-
- async def test_get_help_command_command_specified(self):
- """Should return coroutine of help command of specified command."""
- self.ctx.command = "foo"
- result = ErrorHandler.get_help_command(self.ctx)
- expected = self.ctx.send_help("foo")
- self.assertEqual(result.__qualname__, expected.__qualname__)
- self.assertEqual(result.cr_frame.f_locals, expected.cr_frame.f_locals)
-
- # Await coroutines to avoid warnings
- await result
- await expected
-
- async def test_get_help_command_no_command_specified(self):
- """Should return coroutine of help command."""
- self.ctx.command = None
- result = ErrorHandler.get_help_command(self.ctx)
- expected = self.ctx.send_help()
- self.assertEqual(result.__qualname__, expected.__qualname__)
- self.assertEqual(result.cr_frame.f_locals, expected.cr_frame.f_locals)
-
- # Await coroutines to avoid warnings
- await result
- await expected
-
-
class ErrorHandlerSetupTests(unittest.TestCase):
"""Tests for `ErrorHandler` `setup` function."""
diff --git a/tests/bot/exts/events/test_code_jams.py b/tests/bot/exts/events/test_code_jams.py
index 0856546af..fdff36b61 100644
--- a/tests/bot/exts/events/test_code_jams.py
+++ b/tests/bot/exts/events/test_code_jams.py
@@ -1,8 +1,8 @@
import unittest
from unittest.mock import AsyncMock, MagicMock, create_autospec, patch
-from discord import CategoryChannel
-from discord.ext.commands import BadArgument
+from disnake import CategoryChannel
+from disnake.ext.commands import BadArgument
from bot.constants import Roles
from bot.exts.events import code_jams
diff --git a/tests/bot/exts/filters/test_antimalware.py b/tests/bot/exts/filters/test_antimalware.py
index 06d78de9d..0cab405d0 100644
--- a/tests/bot/exts/filters/test_antimalware.py
+++ b/tests/bot/exts/filters/test_antimalware.py
@@ -1,7 +1,7 @@
import unittest
from unittest.mock import AsyncMock, Mock
-from discord import NotFound
+from disnake import NotFound
from bot.constants import Channels, STAFF_ROLES
from bot.exts.filters import antimalware
diff --git a/tests/bot/exts/filters/test_security.py b/tests/bot/exts/filters/test_security.py
index c0c3baa42..46fa82fd7 100644
--- a/tests/bot/exts/filters/test_security.py
+++ b/tests/bot/exts/filters/test_security.py
@@ -1,7 +1,7 @@
import unittest
from unittest.mock import MagicMock
-from discord.ext.commands import NoPrivateMessage
+from disnake.ext.commands import NoPrivateMessage
from bot.exts.filters import security
from tests.helpers import MockBot, MockContext
diff --git a/tests/bot/exts/filters/test_token_remover.py b/tests/bot/exts/filters/test_token_remover.py
index 4db27269a..dd56c10dd 100644
--- a/tests/bot/exts/filters/test_token_remover.py
+++ b/tests/bot/exts/filters/test_token_remover.py
@@ -3,7 +3,7 @@ from re import Match
from unittest import mock
from unittest.mock import MagicMock
-from discord import Colour, NotFound
+from disnake import Colour, NotFound
from bot import constants
from bot.exts.filters import token_remover
diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py
index 632287322..9a35de7a9 100644
--- a/tests/bot/exts/info/test_information.py
+++ b/tests/bot/exts/info/test_information.py
@@ -1,8 +1,9 @@
import textwrap
import unittest
import unittest.mock
+from datetime import datetime
-import discord
+import disnake
from bot import constants
from bot.exts.info import information
@@ -42,7 +43,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase):
embed = kwargs.pop('embed')
self.assertEqual(embed.title, "Role information (Total 1 role)")
- self.assertEqual(embed.colour, discord.Colour.og_blurple())
+ self.assertEqual(embed.colour, disnake.Colour.og_blurple())
self.assertEqual(embed.description, f"\n`{self.moderator_role.id}` - {self.moderator_role.mention}\n")
async def test_role_info_command(self):
@@ -50,19 +51,19 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase):
dummy_role = helpers.MockRole(
name="Dummy",
id=112233445566778899,
- colour=discord.Colour.og_blurple(),
+ colour=disnake.Colour.og_blurple(),
position=10,
members=[self.ctx.author],
- permissions=discord.Permissions(0)
+ permissions=disnake.Permissions(0)
)
admin_role = helpers.MockRole(
name="Admins",
id=998877665544332211,
- colour=discord.Colour.red(),
+ colour=disnake.Colour.red(),
position=3,
members=[self.ctx.author],
- permissions=discord.Permissions(0),
+ permissions=disnake.Permissions(0),
)
self.ctx.guild.roles.extend([dummy_role, admin_role])
@@ -80,7 +81,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase):
admin_embed = admin_kwargs["embed"]
self.assertEqual(dummy_embed.title, "Dummy info")
- self.assertEqual(dummy_embed.colour, discord.Colour.og_blurple())
+ self.assertEqual(dummy_embed.colour, disnake.Colour.og_blurple())
self.assertEqual(dummy_embed.fields[0].value, str(dummy_role.id))
self.assertEqual(dummy_embed.fields[1].value, f"#{dummy_role.colour.value:0>6x}")
@@ -90,7 +91,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(dummy_embed.fields[5].value, "0")
self.assertEqual(admin_embed.title, "Admins info")
- self.assertEqual(admin_embed.colour, discord.Colour.red())
+ self.assertEqual(admin_embed.colour, disnake.Colour.red())
class UserInfractionHelperMethodTests(unittest.IsolatedAsyncioTestCase):
@@ -276,6 +277,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
f"{COG_PATH}.basic_user_infraction_counts",
new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions"))
)
+ @unittest.mock.patch(
+ f"{COG_PATH}.user_messages",
+ new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count"))
+ )
async def test_create_user_embed_uses_string_representation_of_user_in_title_if_nick_is_not_available(self):
"""The embed should use the string representation of the user if they don't have a nick."""
ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1))
@@ -284,8 +289,9 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
user.nick = None
user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock")
user.colour = 0
+ user.created_at = user.joined_at = datetime.utcnow()
- embed = await self.cog.create_user_embed(ctx, user)
+ embed = await self.cog.create_user_embed(ctx, user, False)
self.assertEqual(embed.title, "Mr. Hemlock")
@@ -293,6 +299,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
f"{COG_PATH}.basic_user_infraction_counts",
new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions"))
)
+ @unittest.mock.patch(
+ f"{COG_PATH}.user_messages",
+ new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count"))
+ )
async def test_create_user_embed_uses_nick_in_title_if_available(self):
"""The embed should use the nick if it's available."""
ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1))
@@ -301,8 +311,9 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
user.nick = "Cat lover"
user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock")
user.colour = 0
+ user.created_at = user.joined_at = datetime.utcnow()
- embed = await self.cog.create_user_embed(ctx, user)
+ embed = await self.cog.create_user_embed(ctx, user, False)
self.assertEqual(embed.title, "Cat lover (Mr. Hemlock)")
@@ -310,6 +321,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
f"{COG_PATH}.basic_user_infraction_counts",
new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions"))
)
+ @unittest.mock.patch(
+ f"{COG_PATH}.user_messages",
+ new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count"))
+ )
async def test_create_user_embed_ignores_everyone_role(self):
"""Created `!user` embeds should not contain mention of the @everyone-role."""
ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1))
@@ -317,14 +332,19 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
# A `MockMember` has the @Everyone role by default; we add the Admins to that.
user = helpers.MockMember(roles=[admins_role], colour=100)
+ user.created_at = user.joined_at = datetime.utcnow()
- embed = await self.cog.create_user_embed(ctx, user)
+ embed = await self.cog.create_user_embed(ctx, user, False)
self.assertIn("&Admins", embed.fields[1].value)
self.assertNotIn("&Everyone", embed.fields[1].value)
@unittest.mock.patch(f"{COG_PATH}.expanded_user_infraction_counts", new_callable=unittest.mock.AsyncMock)
@unittest.mock.patch(f"{COG_PATH}.user_nomination_counts", new_callable=unittest.mock.AsyncMock)
+ @unittest.mock.patch(
+ f"{COG_PATH}.user_messages",
+ new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count"))
+ )
async def test_create_user_embed_expanded_information_in_moderation_channels(
self,
nomination_counts,
@@ -339,7 +359,8 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
nomination_counts.return_value = ("Nominations", "nomination info")
user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)
- embed = await self.cog.create_user_embed(ctx, user)
+ user.created_at = user.joined_at = datetime.utcfromtimestamp(1)
+ embed = await self.cog.create_user_embed(ctx, user, False)
infraction_counts.assert_called_once_with(user)
nomination_counts.assert_called_once_with(user)
@@ -363,16 +384,23 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
)
@unittest.mock.patch(f"{COG_PATH}.basic_user_infraction_counts", new_callable=unittest.mock.AsyncMock)
- async def test_create_user_embed_basic_information_outside_of_moderation_channels(self, infraction_counts):
+ @unittest.mock.patch(f"{COG_PATH}.user_messages", new_callable=unittest.mock.AsyncMock)
+ async def test_create_user_embed_basic_information_outside_of_moderation_channels(
+ self,
+ user_messages,
+ infraction_counts,
+ ):
"""The embed should contain only basic infraction data outside of mod channels."""
ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=100))
moderators_role = helpers.MockRole(name='Moderators')
infraction_counts.return_value = ("Infractions", "basic infractions info")
+ user_messages.return_value = ("Messages", "user message counts")
user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)
- embed = await self.cog.create_user_embed(ctx, user)
+ user.created_at = user.joined_at = datetime.utcfromtimestamp(1)
+ embed = await self.cog.create_user_embed(ctx, user, False)
infraction_counts.assert_called_once_with(user)
@@ -394,14 +422,23 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
)
self.assertEqual(
- "basic infractions info",
+ "user message counts",
embed.fields[2].value
)
+ self.assertEqual(
+ "basic infractions info",
+ embed.fields[3].value
+ )
+
@unittest.mock.patch(
f"{COG_PATH}.basic_user_infraction_counts",
new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions"))
)
+ @unittest.mock.patch(
+ f"{COG_PATH}.user_messages",
+ new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count"))
+ )
async def test_create_user_embed_uses_top_role_colour_when_user_has_roles(self):
"""The embed should be created with the colour of the top role, if a top role is available."""
ctx = helpers.MockContext()
@@ -409,34 +446,45 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
moderators_role = helpers.MockRole(name='Moderators')
user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)
- embed = await self.cog.create_user_embed(ctx, user)
+ user.created_at = user.joined_at = datetime.utcnow()
+ embed = await self.cog.create_user_embed(ctx, user, False)
- self.assertEqual(embed.colour, discord.Colour(100))
+ self.assertEqual(embed.colour, disnake.Colour(100))
@unittest.mock.patch(
f"{COG_PATH}.basic_user_infraction_counts",
new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions"))
)
+ @unittest.mock.patch(
+ f"{COG_PATH}.user_messages",
+ new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count"))
+ )
async def test_create_user_embed_uses_og_blurple_colour_when_user_has_no_roles(self):
"""The embed should be created with the og blurple colour if the user has no assigned roles."""
ctx = helpers.MockContext()
- user = helpers.MockMember(id=217, colour=discord.Colour.default())
- embed = await self.cog.create_user_embed(ctx, user)
+ user = helpers.MockMember(id=217, colour=disnake.Colour.default())
+ user.created_at = user.joined_at = datetime.utcnow()
+ embed = await self.cog.create_user_embed(ctx, user, False)
- self.assertEqual(embed.colour, discord.Colour.og_blurple())
+ self.assertEqual(embed.colour, disnake.Colour.og_blurple())
@unittest.mock.patch(
f"{COG_PATH}.basic_user_infraction_counts",
new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions"))
)
+ @unittest.mock.patch(
+ f"{COG_PATH}.user_messages",
+ new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count"))
+ )
async def test_create_user_embed_uses_png_format_of_user_avatar_as_thumbnail(self):
"""The embed thumbnail should be set to the user's avatar in `png` format."""
ctx = helpers.MockContext()
user = helpers.MockMember(id=217, colour=0)
+ user.created_at = user.joined_at = datetime.utcnow()
user.display_avatar.url = "avatar url"
- embed = await self.cog.create_user_embed(ctx, user)
+ embed = await self.cog.create_user_embed(ctx, user, False)
self.assertEqual(embed.thumbnail.url, "avatar url")
@@ -489,7 +537,7 @@ class UserCommandTests(unittest.IsolatedAsyncioTestCase):
await self.cog.user_info(self.cog, ctx)
- create_embed.assert_called_once_with(ctx, self.author)
+ create_embed.assert_called_once_with(ctx, self.author, False)
ctx.send.assert_called_once()
@unittest.mock.patch("bot.exts.info.information.Information.create_user_embed")
@@ -500,7 +548,7 @@ class UserCommandTests(unittest.IsolatedAsyncioTestCase):
await self.cog.user_info(self.cog, ctx, self.author)
- create_embed.assert_called_once_with(ctx, self.author)
+ create_embed.assert_called_once_with(ctx, self.author, False)
ctx.send.assert_called_once()
@unittest.mock.patch("bot.exts.info.information.Information.create_user_embed")
@@ -511,7 +559,7 @@ class UserCommandTests(unittest.IsolatedAsyncioTestCase):
await self.cog.user_info(self.cog, ctx)
- create_embed.assert_called_once_with(ctx, self.moderator)
+ create_embed.assert_called_once_with(ctx, self.moderator, False)
ctx.send.assert_called_once()
@unittest.mock.patch("bot.exts.info.information.Information.create_user_embed")
@@ -523,5 +571,5 @@ class UserCommandTests(unittest.IsolatedAsyncioTestCase):
await self.cog.user_info(self.cog, ctx, self.target)
- create_embed.assert_called_once_with(ctx, self.target)
+ create_embed.assert_called_once_with(ctx, self.target, False)
ctx.send.assert_called_once()
diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py
index 4d01e18a5..b85d086c9 100644
--- a/tests/bot/exts/moderation/infraction/test_infractions.py
+++ b/tests/bot/exts/moderation/infraction/test_infractions.py
@@ -1,13 +1,15 @@
import inspect
import textwrap
import unittest
-from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch
+from unittest.mock import ANY, AsyncMock, DEFAULT, MagicMock, Mock, patch
-from discord.errors import NotFound
+from disnake.errors import NotFound
from bot.constants import Event
+from bot.exts.moderation.clean import Clean
from bot.exts.moderation.infraction import _utils
from bot.exts.moderation.infraction.infractions import Infractions
+from bot.exts.moderation.infraction.management import ModManagement
from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockUser, autospec
@@ -62,8 +64,8 @@ class TruncationTests(unittest.IsolatedAsyncioTestCase):
@patch("bot.exts.moderation.infraction.infractions.constants.Roles.voice_verified", new=123456)
-class VoiceBanTests(unittest.IsolatedAsyncioTestCase):
- """Tests for voice ban related functions and commands."""
+class VoiceMuteTests(unittest.IsolatedAsyncioTestCase):
+ """Tests for voice mute related functions and commands."""
def setUp(self):
self.bot = MockBot()
@@ -73,59 +75,59 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):
self.ctx = MockContext(bot=self.bot, author=self.mod)
self.cog = Infractions(self.bot)
- async def test_permanent_voice_ban(self):
- """Should call voice ban applying function without expiry."""
- self.cog.apply_voice_ban = AsyncMock()
- self.assertIsNone(await self.cog.voiceban(self.cog, self.ctx, self.user, reason="foobar"))
- self.cog.apply_voice_ban.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at=None)
+ async def test_permanent_voice_mute(self):
+ """Should call voice mute applying function without expiry."""
+ self.cog.apply_voice_mute = AsyncMock()
+ self.assertIsNone(await self.cog.voicemute(self.cog, self.ctx, self.user, reason="foobar"))
+ self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at=None)
- async def test_temporary_voice_ban(self):
- """Should call voice ban applying function with expiry."""
- self.cog.apply_voice_ban = AsyncMock()
- self.assertIsNone(await self.cog.tempvoiceban(self.cog, self.ctx, self.user, "baz", reason="foobar"))
- self.cog.apply_voice_ban.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at="baz")
+ async def test_temporary_voice_mute(self):
+ """Should call voice mute applying function with expiry."""
+ self.cog.apply_voice_mute = AsyncMock()
+ self.assertIsNone(await self.cog.tempvoicemute(self.cog, self.ctx, self.user, "baz", reason="foobar"))
+ self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at="baz")
- async def test_voice_unban(self):
+ async def test_voice_unmute(self):
"""Should call infraction pardoning function."""
self.cog.pardon_infraction = AsyncMock()
- self.assertIsNone(await self.cog.unvoiceban(self.cog, self.ctx, self.user))
- self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_ban", self.user)
+ self.assertIsNone(await self.cog.unvoicemute(self.cog, self.ctx, self.user))
+ self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_mute", self.user)
@patch("bot.exts.moderation.infraction.infractions._utils.post_infraction")
@patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction")
- async def test_voice_ban_user_have_active_infraction(self, get_active_infraction, post_infraction_mock):
- """Should return early when user already have Voice Ban infraction."""
+ async def test_voice_mute_user_have_active_infraction(self, get_active_infraction, post_infraction_mock):
+ """Should return early when user already have Voice Mute infraction."""
get_active_infraction.return_value = {"foo": "bar"}
- self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar"))
- get_active_infraction.assert_awaited_once_with(self.ctx, self.user, "voice_ban")
+ self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, "foobar"))
+ get_active_infraction.assert_awaited_once_with(self.ctx, self.user, "voice_mute")
post_infraction_mock.assert_not_awaited()
@patch("bot.exts.moderation.infraction.infractions._utils.post_infraction")
@patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction")
- async def test_voice_ban_infraction_post_failed(self, get_active_infraction, post_infraction_mock):
+ async def test_voice_mute_infraction_post_failed(self, get_active_infraction, post_infraction_mock):
"""Should return early when posting infraction fails."""
self.cog.mod_log.ignore = MagicMock()
get_active_infraction.return_value = None
post_infraction_mock.return_value = None
- self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar"))
+ self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, "foobar"))
post_infraction_mock.assert_awaited_once()
self.cog.mod_log.ignore.assert_not_called()
@patch("bot.exts.moderation.infraction.infractions._utils.post_infraction")
@patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction")
- async def test_voice_ban_infraction_post_add_kwargs(self, get_active_infraction, post_infraction_mock):
- """Should pass all kwargs passed to apply_voice_ban to post_infraction."""
+ async def test_voice_mute_infraction_post_add_kwargs(self, get_active_infraction, post_infraction_mock):
+ """Should pass all kwargs passed to apply_voice_mute to post_infraction."""
get_active_infraction.return_value = None
# We don't want that this continue yet
post_infraction_mock.return_value = None
- self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar", my_kwarg=23))
+ self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, "foobar", my_kwarg=23))
post_infraction_mock.assert_awaited_once_with(
- self.ctx, self.user, "voice_ban", "foobar", active=True, my_kwarg=23
+ self.ctx, self.user, "voice_mute", "foobar", active=True, my_kwarg=23
)
@patch("bot.exts.moderation.infraction.infractions._utils.post_infraction")
@patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction")
- async def test_voice_ban_mod_log_ignore(self, get_active_infraction, post_infraction_mock):
+ async def test_voice_mute_mod_log_ignore(self, get_active_infraction, post_infraction_mock):
"""Should ignore Voice Verified role removing."""
self.cog.mod_log.ignore = MagicMock()
self.cog.apply_infraction = AsyncMock()
@@ -134,11 +136,11 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):
get_active_infraction.return_value = None
post_infraction_mock.return_value = {"foo": "bar"}
- self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar"))
+ self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, "foobar"))
self.cog.mod_log.ignore.assert_called_once_with(Event.member_update, self.user.id)
async def action_tester(self, action, reason: str) -> None:
- """Helper method to test voice ban action."""
+ """Helper method to test voice mute action."""
self.assertTrue(inspect.iscoroutine(action))
await action
@@ -147,7 +149,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):
@patch("bot.exts.moderation.infraction.infractions._utils.post_infraction")
@patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction")
- async def test_voice_ban_apply_infraction(self, get_active_infraction, post_infraction_mock):
+ async def test_voice_mute_apply_infraction(self, get_active_infraction, post_infraction_mock):
"""Should ignore Voice Verified role removing."""
self.cog.mod_log.ignore = MagicMock()
self.cog.apply_infraction = AsyncMock()
@@ -156,22 +158,22 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):
post_infraction_mock.return_value = {"foo": "bar"}
reason = "foobar"
- self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, reason))
+ self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, reason))
self.cog.apply_infraction.assert_awaited_once_with(self.ctx, {"foo": "bar"}, self.user, ANY)
await self.action_tester(self.cog.apply_infraction.call_args[0][-1], reason)
@patch("bot.exts.moderation.infraction.infractions._utils.post_infraction")
@patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction")
- async def test_voice_ban_truncate_reason(self, get_active_infraction, post_infraction_mock):
- """Should truncate reason for voice ban."""
+ async def test_voice_mute_truncate_reason(self, get_active_infraction, post_infraction_mock):
+ """Should truncate reason for voice mute."""
self.cog.mod_log.ignore = MagicMock()
self.cog.apply_infraction = AsyncMock()
get_active_infraction.return_value = None
post_infraction_mock.return_value = {"foo": "bar"}
- self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar" * 3000))
+ self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, "foobar" * 3000))
self.cog.apply_infraction.assert_awaited_once_with(self.ctx, {"foo": "bar"}, self.user, ANY)
# Test action
@@ -180,14 +182,14 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):
@autospec(_utils, "post_infraction", "get_active_infraction", return_value=None)
@autospec(Infractions, "apply_infraction")
- async def test_voice_ban_user_left_guild(self, apply_infraction_mock, post_infraction_mock, _):
- """Should voice ban user that left the guild without throwing an error."""
+ async def test_voice_mute_user_left_guild(self, apply_infraction_mock, post_infraction_mock, _):
+ """Should voice mute user that left the guild without throwing an error."""
infraction = {"foo": "bar"}
post_infraction_mock.return_value = {"foo": "bar"}
user = MockUser()
- await self.cog.voiceban(self.cog, self.ctx, user, reason=None)
- post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_ban", None, active=True, expires_at=None)
+ await self.cog.voicemute(self.cog, self.ctx, user, reason=None)
+ post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_mute", None, active=True, expires_at=None)
apply_infraction_mock.assert_called_once_with(self.cog, self.ctx, infraction, user, ANY)
# Test action
@@ -195,22 +197,22 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):
self.assertTrue(inspect.iscoroutine(action))
await action
- async def test_voice_unban_user_not_found(self):
+ async def test_voice_unmute_user_not_found(self):
"""Should include info to return dict when user was not found from guild."""
self.guild.get_member.return_value = None
self.guild.fetch_member.side_effect = NotFound(Mock(status=404), "Not found")
- result = await self.cog.pardon_voice_ban(self.user.id, self.guild)
+ result = await self.cog.pardon_voice_mute(self.user.id, self.guild)
self.assertEqual(result, {"Info": "User was not found in the guild."})
@patch("bot.exts.moderation.infraction.infractions._utils.notify_pardon")
@patch("bot.exts.moderation.infraction.infractions.format_user")
- async def test_voice_unban_user_found(self, format_user_mock, notify_pardon_mock):
+ async def test_voice_unmute_user_found(self, format_user_mock, notify_pardon_mock):
"""Should add role back with ignoring, notify user and return log dictionary.."""
self.guild.get_member.return_value = self.user
notify_pardon_mock.return_value = True
format_user_mock.return_value = "my-user"
- result = await self.cog.pardon_voice_ban(self.user.id, self.guild)
+ result = await self.cog.pardon_voice_mute(self.user.id, self.guild)
self.assertEqual(result, {
"Member": "my-user",
"DM": "Sent"
@@ -219,15 +221,100 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):
@patch("bot.exts.moderation.infraction.infractions._utils.notify_pardon")
@patch("bot.exts.moderation.infraction.infractions.format_user")
- async def test_voice_unban_dm_fail(self, format_user_mock, notify_pardon_mock):
+ async def test_voice_unmute_dm_fail(self, format_user_mock, notify_pardon_mock):
"""Should add role back with ignoring, notify user and return log dictionary.."""
self.guild.get_member.return_value = self.user
notify_pardon_mock.return_value = False
format_user_mock.return_value = "my-user"
- result = await self.cog.pardon_voice_ban(self.user.id, self.guild)
+ result = await self.cog.pardon_voice_mute(self.user.id, self.guild)
self.assertEqual(result, {
"Member": "my-user",
"DM": "**Failed**"
})
notify_pardon_mock.assert_awaited_once()
+
+
+class CleanBanTests(unittest.IsolatedAsyncioTestCase):
+ """Tests for cleanban functionality."""
+
+ def setUp(self):
+ self.bot = MockBot()
+ self.mod = MockMember(roles=[MockRole(id=7890123, position=10)])
+ self.user = MockMember(roles=[MockRole(id=123456, position=1)])
+ self.guild = MockGuild()
+ self.ctx = MockContext(bot=self.bot, author=self.mod)
+ self.cog = Infractions(self.bot)
+ self.clean_cog = Clean(self.bot)
+ self.management_cog = ModManagement(self.bot)
+
+ self.cog.apply_ban = AsyncMock(return_value={"id": 42})
+ self.log_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
+ self.clean_cog._clean_messages = AsyncMock(return_value=self.log_url)
+
+ def mock_get_cog(self, enable_clean, enable_manage):
+ """Mock get cog factory that allows the user to specify whether clean and manage cogs are enabled."""
+ def inner(name):
+ if name == "ModManagement":
+ return self.management_cog if enable_manage else None
+ elif name == "Clean":
+ return self.clean_cog if enable_clean else None
+ else:
+ return DEFAULT
+ return inner
+
+ async def test_cleanban_falls_back_to_native_purge_without_clean_cog(self):
+ """Should fallback to native purge if the Clean cog is not available."""
+ self.bot.get_cog.side_effect = self.mock_get_cog(False, False)
+
+ self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar"))
+ self.cog.apply_ban.assert_awaited_once_with(
+ self.ctx,
+ self.user,
+ "FooBar",
+ purge_days=1,
+ expires_at=None,
+ )
+
+ async def test_cleanban_doesnt_purge_messages_if_clean_cog_available(self):
+ """Cleanban command should use the native purge messages if the clean cog is available."""
+ self.bot.get_cog.side_effect = self.mock_get_cog(True, False)
+
+ self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar"))
+ self.cog.apply_ban.assert_awaited_once_with(
+ self.ctx,
+ self.user,
+ "FooBar",
+ expires_at=None,
+ )
+
+ @patch("bot.exts.moderation.infraction.infractions.Age")
+ async def test_cleanban_uses_clean_cog_when_available(self, mocked_age_converter):
+ """Test cleanban uses the clean cog to clean messages if it's available."""
+ self.bot.api_client.patch = AsyncMock()
+ self.bot.get_cog.side_effect = self.mock_get_cog(True, False)
+
+ mocked_age_converter.return_value.convert = AsyncMock(return_value="81M")
+ self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar"))
+
+ self.clean_cog._clean_messages.assert_awaited_once_with(
+ self.ctx,
+ users=[self.user],
+ channels="*",
+ first_limit="81M",
+ attempt_delete_invocation=False,
+ )
+
+ async def test_cleanban_edits_infraction_reason(self):
+ """Ensure cleanban edits the ban reason with a link to the clean log."""
+ self.bot.get_cog.side_effect = self.mock_get_cog(True, True)
+
+ self.management_cog.infraction_append = AsyncMock()
+ self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar"))
+
+ self.management_cog.infraction_append.assert_awaited_once_with(
+ self.ctx,
+ {"id": 42},
+ None,
+ reason=f"[Clean log]({self.log_url})"
+ )
diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py
index 72eebb254..eaa0e701e 100644
--- a/tests/bot/exts/moderation/infraction/test_utils.py
+++ b/tests/bot/exts/moderation/infraction/test_utils.py
@@ -3,7 +3,7 @@ from collections import namedtuple
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, call, patch
-from discord import Embed, Forbidden, HTTPException, NotFound
+from disnake import Embed, Forbidden, HTTPException, NotFound
from bot.api import ResponseCodeError
from bot.constants import Colours, Icons
@@ -15,7 +15,10 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
"""Tests Moderation utils."""
def setUp(self):
- self.bot = MockBot()
+ patcher = patch("bot.instance", new=MockBot())
+ self.bot = patcher.start()
+ self.addCleanup(patcher.stop)
+
self.member = MockMember(id=1234)
self.user = MockUser(id=1234)
self.ctx = MockContext(bot=self.bot, author=self.member)
@@ -123,8 +126,9 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
else:
self.ctx.send.assert_not_awaited()
+ @unittest.skip("Current time needs to be patched so infraction duration is correct.")
@patch("bot.exts.moderation.infraction._utils.send_private_embed")
- async def test_notify_infraction(self, send_private_embed_mock):
+ async def test_send_infraction_embed(self, send_private_embed_mock):
"""
Should send an embed of a certain format as a DM and return `True` if DM successful.
@@ -132,7 +136,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
"""
test_cases = [
{
- "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"),
+ "args": (dict(id=0, type="ban", reason=None, expires_at=datetime(2020, 2, 26, 9, 20)), self.user),
"expected_output": Embed(
title=utils.INFRACTION_TITLE,
description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
@@ -145,12 +149,12 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
).set_author(
name=utils.INFRACTION_AUTHOR_NAME,
url=utils.RULES_URL,
- icon_url=Icons.token_removed
+ icon_url=Icons.user_ban
),
"send_result": True
},
{
- "args": (self.user, "warning", None, "Test reason."),
+ "args": (dict(id=0, type="warning", reason="Test reason.", expires_at=None), self.user),
"expected_output": Embed(
title=utils.INFRACTION_TITLE,
description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
@@ -163,14 +167,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
).set_author(
name=utils.INFRACTION_AUTHOR_NAME,
url=utils.RULES_URL,
- icon_url=Icons.token_removed
+ icon_url=Icons.user_warn
),
"send_result": False
},
# Note that this test case asserts that the DM that *would* get sent to the user is formatted
# correctly, even though that message is deliberately never sent.
{
- "args": (self.user, "note", None, None, Icons.defcon_denied),
+ "args": (dict(id=0, type="note", reason=None, expires_at=None), self.user),
"expected_output": Embed(
title=utils.INFRACTION_TITLE,
description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
@@ -183,12 +187,12 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
).set_author(
name=utils.INFRACTION_AUTHOR_NAME,
url=utils.RULES_URL,
- icon_url=Icons.defcon_denied
+ icon_url=Icons.user_warn
),
"send_result": False
},
{
- "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied),
+ "args": (dict(id=0, type="mute", reason="Test", expires_at=datetime(2020, 2, 26, 9, 20)), self.user),
"expected_output": Embed(
title=utils.INFRACTION_TITLE,
description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
@@ -201,12 +205,12 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
).set_author(
name=utils.INFRACTION_AUTHOR_NAME,
url=utils.RULES_URL,
- icon_url=Icons.defcon_denied
+ icon_url=Icons.user_mute
),
"send_result": False
},
{
- "args": (self.user, "mute", None, "foo bar" * 4000, Icons.defcon_denied),
+ "args": (dict(id=0, type="mute", reason="foo bar" * 4000, expires_at=None), self.user),
"expected_output": Embed(
title=utils.INFRACTION_TITLE,
description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
@@ -219,7 +223,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
).set_author(
name=utils.INFRACTION_AUTHOR_NAME,
url=utils.RULES_URL,
- icon_url=Icons.defcon_denied
+ icon_url=Icons.user_mute
),
"send_result": True
}
@@ -238,7 +242,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(embed.to_dict(), case["expected_output"].to_dict())
- send_private_embed_mock.assert_awaited_once_with(case["args"][0], embed)
+ send_private_embed_mock.assert_awaited_once_with(case["args"][1], embed)
@patch("bot.exts.moderation.infraction._utils.send_private_embed")
async def test_notify_pardon(self, send_private_embed_mock):
@@ -313,7 +317,8 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase):
"type": "ban",
"user": self.member.id,
"active": False,
- "expires_at": now.isoformat()
+ "expires_at": now.isoformat(),
+ "dm_sent": False
}
self.ctx.bot.api_client.post.return_value = "foo"
@@ -350,7 +355,8 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase):
"reason": "Test reason",
"type": "mute",
"user": self.user.id,
- "active": True
+ "active": True,
+ "dm_sent": False
}
self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"]
diff --git a/tests/bot/exts/moderation/test_clean.py b/tests/bot/exts/moderation/test_clean.py
new file mode 100644
index 000000000..d7647fa48
--- /dev/null
+++ b/tests/bot/exts/moderation/test_clean.py
@@ -0,0 +1,104 @@
+import unittest
+from unittest.mock import AsyncMock, MagicMock, patch
+
+from bot.exts.moderation.clean import Clean
+from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockMessage, MockRole, MockTextChannel
+
+
+class CleanTests(unittest.IsolatedAsyncioTestCase):
+ """Tests for clean cog functionality."""
+
+ def setUp(self):
+ self.bot = MockBot()
+ self.mod = MockMember(roles=[MockRole(id=7890123, position=10)])
+ self.user = MockMember(roles=[MockRole(id=123456, position=1)])
+ self.guild = MockGuild()
+ self.ctx = MockContext(bot=self.bot, author=self.mod)
+ self.cog = Clean(self.bot)
+
+ self.log_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
+ self.cog._modlog_cleaned_messages = AsyncMock(return_value=self.log_url)
+
+ self.cog._use_cache = MagicMock(return_value=True)
+ self.cog._delete_found = AsyncMock(return_value=[42, 84])
+
+ @patch("bot.exts.moderation.clean.is_mod_channel")
+ async def test_clean_deletes_invocation_in_non_mod_channel(self, mod_channel_check):
+ """Clean command should delete the invocation message if ran in a non mod channel."""
+ mod_channel_check.return_value = False
+ self.ctx.message.delete = AsyncMock()
+
+ self.assertIsNone(await self.cog._delete_invocation(self.ctx))
+
+ self.ctx.message.delete.assert_awaited_once()
+
+ @patch("bot.exts.moderation.clean.is_mod_channel")
+ async def test_clean_doesnt_delete_invocation_in_mod_channel(self, mod_channel_check):
+ """Clean command should not delete the invocation message if ran in a mod channel."""
+ mod_channel_check.return_value = True
+ self.ctx.message.delete = AsyncMock()
+
+ self.assertIsNone(await self.cog._delete_invocation(self.ctx))
+
+ self.ctx.message.delete.assert_not_awaited()
+
+ async def test_clean_doesnt_attempt_deletion_when_attempt_delete_invocation_is_false(self):
+ """Clean command should not attempt to delete the invocation message if attempt_delete_invocation is false."""
+ self.cog._delete_invocation = AsyncMock()
+ self.bot.get_channel = MagicMock(return_value=False)
+
+ self.assertEqual(
+ await self.cog._clean_messages(
+ self.ctx,
+ None,
+ first_limit=MockMessage(),
+ attempt_delete_invocation=False,
+ ),
+ self.log_url,
+ )
+
+ self.cog._delete_invocation.assert_not_awaited()
+
+ @patch("bot.exts.moderation.clean.is_mod_channel")
+ async def test_clean_replies_with_success_message_when_ran_in_mod_channel(self, mod_channel_check):
+ """Clean command should reply to the message with a confirmation message if invoked in a mod channel."""
+ mod_channel_check.return_value = True
+ self.ctx.reply = AsyncMock()
+
+ self.assertEqual(
+ await self.cog._clean_messages(
+ self.ctx,
+ None,
+ first_limit=MockMessage(),
+ attempt_delete_invocation=False,
+ ),
+ self.log_url,
+ )
+
+ self.ctx.reply.assert_awaited_once()
+ sent_message = self.ctx.reply.await_args[0][0]
+ self.assertIn(self.log_url, sent_message)
+ self.assertIn("2 messages", sent_message)
+
+ @patch("bot.exts.moderation.clean.is_mod_channel")
+ async def test_clean_send_success_message_to_mods_when_ran_in_non_mod_channel(self, mod_channel_check):
+ """Clean command should send a confirmation message to #mods if invoked in a non-mod channel."""
+ mod_channel_check.return_value = False
+ mocked_mods = MockTextChannel(id=1234567)
+ mocked_mods.send = AsyncMock()
+ self.bot.get_channel = MagicMock(return_value=mocked_mods)
+
+ self.assertEqual(
+ await self.cog._clean_messages(
+ self.ctx,
+ None,
+ first_limit=MockMessage(),
+ attempt_delete_invocation=False,
+ ),
+ self.log_url,
+ )
+
+ mocked_mods.send.assert_awaited_once()
+ sent_message = mocked_mods.send.await_args[0][0]
+ self.assertIn(self.log_url, sent_message)
+ self.assertIn("2 messages", sent_message)
diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py
index cfe0c4b03..725455bbe 100644
--- a/tests/bot/exts/moderation/test_incidents.py
+++ b/tests/bot/exts/moderation/test_incidents.py
@@ -7,7 +7,7 @@ from unittest import mock
from unittest.mock import AsyncMock, MagicMock, Mock, call, patch
import aiohttp
-import discord
+import disnake
from async_rediscache import RedisSession
from bot.constants import Colours
@@ -24,7 +24,7 @@ class MockAsyncIterable:
Helper for mocking asynchronous for loops.
It does not appear that the `unittest` library currently provides anything that would
- allow us to simply mock an async iterator, such as `discord.TextChannel.history`.
+ allow us to simply mock an async iterator, such as `disnake.TextChannel.history`.
We therefore write our own helper to wrap a regular synchronous iterable, and feed
its values via `__anext__` rather than `__next__`.
@@ -60,7 +60,7 @@ class MockSignal(enum.Enum):
B = "B"
-mock_404 = discord.NotFound(
+mock_404 = disnake.NotFound(
response=MagicMock(aiohttp.ClientResponse), # Mock the erroneous response
message="Not found",
)
@@ -70,8 +70,8 @@ class TestDownloadFile(unittest.IsolatedAsyncioTestCase):
"""Collection of tests for the `download_file` helper function."""
async def test_download_file_success(self):
- """If `to_file` succeeds, function returns the acquired `discord.File`."""
- file = MagicMock(discord.File, filename="bigbadlemon.jpg")
+ """If `to_file` succeeds, function returns the acquired `disnake.File`."""
+ file = MagicMock(disnake.File, filename="bigbadlemon.jpg")
attachment = MockAttachment(to_file=AsyncMock(return_value=file))
acquired_file = await incidents.download_file(attachment)
@@ -86,7 +86,7 @@ class TestDownloadFile(unittest.IsolatedAsyncioTestCase):
async def test_download_file_fail(self):
"""If `to_file` fails on a non-404 error, function logs the exception & returns None."""
- arbitrary_error = discord.HTTPException(MagicMock(aiohttp.ClientResponse), "Arbitrary API error")
+ arbitrary_error = disnake.HTTPException(MagicMock(aiohttp.ClientResponse), "Arbitrary API error")
attachment = MockAttachment(to_file=AsyncMock(side_effect=arbitrary_error))
with self.assertLogs(logger=incidents.log, level=logging.ERROR):
@@ -121,7 +121,7 @@ class TestMakeEmbed(unittest.IsolatedAsyncioTestCase):
async def test_make_embed_with_attachment_succeeds(self):
"""Incident's attachment is downloaded and displayed in the embed's image field."""
- file = MagicMock(discord.File, filename="bigbadjoe.jpg")
+ file = MagicMock(disnake.File, filename="bigbadjoe.jpg")
attachment = MockAttachment(filename="bigbadjoe.jpg")
incident = MockMessage(content="this is an incident", attachments=[attachment])
@@ -394,7 +394,7 @@ class TestArchive(TestIncidents):
author=MockUser(name="author_name", display_avatar=Mock(url="author_avatar")),
id=123,
)
- built_embed = MagicMock(discord.Embed, id=123) # We patch `make_embed` to return this
+ built_embed = MagicMock(disnake.Embed, id=123) # We patch `make_embed` to return this
with patch("bot.exts.moderation.incidents.make_embed", AsyncMock(return_value=(built_embed, None))):
archive_return = await self.cog_instance.archive(incident, MagicMock(value="A"), MockMember())
@@ -616,7 +616,7 @@ class TestResolveMessage(TestIncidents):
"""
self.cog_instance.bot._connection._get_message = MagicMock(return_value=None) # Cache returns None
- arbitrary_error = discord.HTTPException(
+ arbitrary_error = disnake.HTTPException(
response=MagicMock(aiohttp.ClientResponse),
message="Arbitrary error",
)
@@ -649,7 +649,7 @@ class TestOnRawReactionAdd(TestIncidents):
super().setUp() # Ensure `cog_instance` is assigned
self.payload = MagicMock(
- discord.RawReactionActionEvent,
+ disnake.RawReactionActionEvent,
channel_id=123, # Patched at class level
message_id=456,
member=MockMember(bot=False),
diff --git a/tests/bot/exts/moderation/test_modlog.py b/tests/bot/exts/moderation/test_modlog.py
index 79e04837d..6c9ebed95 100644
--- a/tests/bot/exts/moderation/test_modlog.py
+++ b/tests/bot/exts/moderation/test_modlog.py
@@ -1,6 +1,6 @@
import unittest
-import discord
+import disnake
from bot.exts.moderation.modlog import ModLog
from tests.helpers import MockBot, MockTextChannel
@@ -19,7 +19,7 @@ class ModLogTests(unittest.IsolatedAsyncioTestCase):
self.bot.get_channel.return_value = self.channel
await self.cog.send_log_message(
icon_url="foo",
- colour=discord.Colour.blue(),
+ colour=disnake.Colour.blue(),
title="bar",
text="foo bar" * 3000
)
diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py
index 92ce3418a..539651d6c 100644
--- a/tests/bot/exts/moderation/test_silence.py
+++ b/tests/bot/exts/moderation/test_silence.py
@@ -7,7 +7,7 @@ from unittest import mock
from unittest.mock import AsyncMock, Mock
from async_rediscache import RedisSession
-from discord import PermissionOverwrite
+from disnake import PermissionOverwrite
from bot.constants import Channels, Guild, MODERATION_ROLES, Roles
from bot.exts.moderation import silence
@@ -152,7 +152,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase):
# It's too annoying to test cancel_all since it's a done callback and wrapped in a lambda.
self.assertTrue(self.cog._init_task.cancelled())
- @autospec("discord.ext.commands", "has_any_role")
+ @autospec("disnake.ext.commands", "has_any_role")
@mock.patch.object(silence.constants, "MODERATION_ROLES", new=(1, 2, 3))
async def test_cog_check(self, role_check):
"""Role check was called with `MODERATION_ROLES`"""
diff --git a/tests/bot/exts/test_cogs.py b/tests/bot/exts/test_cogs.py
index f8e120262..5cb071d58 100644
--- a/tests/bot/exts/test_cogs.py
+++ b/tests/bot/exts/test_cogs.py
@@ -8,7 +8,7 @@ from collections import defaultdict
from types import ModuleType
from unittest import mock
-from discord.ext import commands
+from disnake.ext import commands
from bot import exts
@@ -34,7 +34,7 @@ class CommandNameTests(unittest.TestCase):
raise ImportError(name=name) # pragma: no cover
# The mock prevents asyncio.get_event_loop() from being called.
- with mock.patch("discord.ext.tasks.loop"):
+ with mock.patch("disnake.ext.tasks.loop"):
prefix = f"{exts.__name__}."
for module in pkgutil.walk_packages(exts.__path__, prefix, onerror=on_error):
if not module.ispkg:
diff --git a/tests/bot/exts/utils/test_snekbox.py b/tests/bot/exts/utils/test_snekbox.py
index 321a92445..bec7574fb 100644
--- a/tests/bot/exts/utils/test_snekbox.py
+++ b/tests/bot/exts/utils/test_snekbox.py
@@ -2,7 +2,8 @@ import asyncio
import unittest
from unittest.mock import AsyncMock, MagicMock, Mock, call, create_autospec, patch
-from discord.ext import commands
+from disnake import AllowedMentions
+from disnake.ext import commands
from bot import constants
from bot.exts.utils import snekbox
@@ -201,7 +202,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
ctx = MockContext()
ctx.message = MockMessage()
ctx.send = AsyncMock()
- ctx.author.mention = '@LemonLemonishBeard#0042'
+ ctx.author = MockUser(mention='@LemonLemonishBeard#0042')
self.cog.post_eval = AsyncMock(return_value={'stdout': '', 'returncode': 0})
self.cog.get_results_message = MagicMock(return_value=('Return code 0', ''))
@@ -213,9 +214,16 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
self.bot.get_cog.return_value = mocked_filter_cog
await self.cog.send_eval(ctx, 'MyAwesomeCode')
- ctx.send.assert_called_once_with(
+
+ ctx.send.assert_called_once()
+ self.assertEqual(
+ ctx.send.call_args.args[0],
'@LemonLemonishBeard#0042 :yay!: Return code 0.\n\n```\n[No output]\n```'
)
+ allowed_mentions = ctx.send.call_args.kwargs['allowed_mentions']
+ expected_allowed_mentions = AllowedMentions(everyone=False, roles=False, users=[ctx.author])
+ self.assertEqual(allowed_mentions.to_dict(), expected_allowed_mentions.to_dict())
+
self.cog.post_eval.assert_called_once_with('MyAwesomeCode')
self.cog.get_status_emoji.assert_called_once_with({'stdout': '', 'returncode': 0})
self.cog.get_results_message.assert_called_once_with({'stdout': '', 'returncode': 0})
@@ -238,10 +246,14 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
self.bot.get_cog.return_value = mocked_filter_cog
await self.cog.send_eval(ctx, 'MyAwesomeCode')
- ctx.send.assert_called_once_with(
+
+ ctx.send.assert_called_once()
+ self.assertEqual(
+ ctx.send.call_args.args[0],
'@LemonLemonishBeard#0042 :yay!: Return code 0.'
'\n\n```\nWay too long beard\n```\nFull output: lookatmybeard.com'
)
+
self.cog.post_eval.assert_called_once_with('MyAwesomeCode')
self.cog.get_status_emoji.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0})
self.cog.get_results_message.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0})
@@ -263,9 +275,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
self.bot.get_cog.return_value = mocked_filter_cog
await self.cog.send_eval(ctx, 'MyAwesomeCode')
- ctx.send.assert_called_once_with(
+
+ ctx.send.assert_called_once()
+ self.assertEqual(
+ ctx.send.call_args.args[0],
'@LemonLemonishBeard#0042 :nope!: Return code 127.\n\n```\nBeard got stuck in the eval\n```'
)
+
self.cog.post_eval.assert_called_once_with('MyAwesomeCode')
self.cog.get_status_emoji.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127})
self.cog.get_results_message.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127})
diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py
index 988b3857b..afb8a973d 100644
--- a/tests/bot/test_converters.py
+++ b/tests/bot/test_converters.py
@@ -4,9 +4,9 @@ from datetime import MAXYEAR, datetime, timezone
from unittest.mock import MagicMock, patch
from dateutil.relativedelta import relativedelta
-from discord.ext.commands import BadArgument
+from disnake.ext.commands import BadArgument
-from bot.converters import Duration, HushDurationConverter, ISODateTime, PackageName, TagNameConverter
+from bot.converters import Duration, HushDurationConverter, ISODateTime, PackageName
class ConverterTests(unittest.IsolatedAsyncioTestCase):
@@ -19,21 +19,6 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):
cls.fixed_utc_now = datetime.fromisoformat('2019-01-01T00:00:00+00:00')
- async def test_tag_name_converter_for_invalid(self):
- """TagNameConverter should raise the correct exception for invalid tag names."""
- test_values = (
- ('👋', "Don't be ridiculous, you can't use that character!"),
- ('', "Tag names should not be empty, or filled with whitespace."),
- (' ', "Tag names should not be empty, or filled with whitespace."),
- ('42', "Tag names must contain at least one letter."),
- ('x' * 128, "Are you insane? That's way too long!"),
- )
-
- for invalid_name, exception_message in test_values:
- with self.subTest(invalid_name=invalid_name, exception_message=exception_message):
- with self.assertRaisesRegex(BadArgument, re.escape(exception_message)):
- await TagNameConverter.convert(self.context, invalid_name)
-
async def test_package_name_for_valid(self):
"""PackageName returns valid package names unchanged."""
test_values = ('foo', 'le_mon', 'num83r')
diff --git a/tests/bot/utils/test_checks.py b/tests/bot/utils/test_checks.py
index 4ae11d5d3..5675e10ec 100644
--- a/tests/bot/utils/test_checks.py
+++ b/tests/bot/utils/test_checks.py
@@ -1,7 +1,7 @@
import unittest
from unittest.mock import MagicMock
-from discord import DMChannel
+from disnake import DMChannel
from bot.utils import checks
from bot.utils.checks import InWhitelistCheckFailure
diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py
index a3dcbfc0a..120d65176 100644
--- a/tests/bot/utils/test_time.py
+++ b/tests/bot/utils/test_time.py
@@ -13,13 +13,15 @@ class TimeTests(unittest.TestCase):
"""humanize_delta should be able to handle unknown units, and will not abort."""
# Does not abort for unknown units, as the unit name is checked
# against the attribute of the relativedelta instance.
- self.assertEqual(time.humanize_delta(relativedelta(days=2, hours=2), 'elephants', 2), '2 days and 2 hours')
+ actual = time.humanize_delta(relativedelta(days=2, hours=2), precision='elephants', max_units=2)
+ self.assertEqual(actual, '2 days and 2 hours')
def test_humanize_delta_handle_high_units(self):
"""humanize_delta should be able to handle very high units."""
# Very high maximum units, but it only ever iterates over
# each value the relativedelta might have.
- self.assertEqual(time.humanize_delta(relativedelta(days=2, hours=2), 'hours', 20), '2 days and 2 hours')
+ actual = time.humanize_delta(relativedelta(days=2, hours=2), precision='hours', max_units=20)
+ self.assertEqual(actual, '2 days and 2 hours')
def test_humanize_delta_should_normal_usage(self):
"""Testing humanize delta."""
@@ -32,7 +34,8 @@ class TimeTests(unittest.TestCase):
for delta, precision, max_units, expected in test_cases:
with self.subTest(delta=delta, precision=precision, max_units=max_units, expected=expected):
- self.assertEqual(time.humanize_delta(delta, precision, max_units), expected)
+ actual = time.humanize_delta(delta, precision=precision, max_units=max_units)
+ self.assertEqual(actual, expected)
def test_humanize_delta_raises_for_invalid_max_units(self):
"""humanize_delta should raises ValueError('max_units must be positive') for invalid max_units."""
@@ -40,22 +43,11 @@ class TimeTests(unittest.TestCase):
for max_units in test_cases:
with self.subTest(max_units=max_units), self.assertRaises(ValueError) as error:
- time.humanize_delta(relativedelta(days=2, hours=2), 'hours', max_units)
- self.assertEqual(str(error.exception), 'max_units must be positive')
-
- def test_parse_rfc1123(self):
- """Testing parse_rfc1123."""
- self.assertEqual(
- time.parse_rfc1123('Sun, 15 Sep 2019 12:00:00 GMT'),
- datetime(2019, 9, 15, 12, 0, 0, tzinfo=timezone.utc)
- )
-
- def test_format_infraction(self):
- """Testing format_infraction."""
- self.assertEqual(time.format_infraction('2019-12-12T00:01:00Z'), '<t:1576108860:f>')
+ time.humanize_delta(relativedelta(days=2, hours=2), precision='hours', max_units=max_units)
+ self.assertEqual(str(error.exception), 'max_units must be positive.')
- def test_format_infraction_with_duration_none_expiry(self):
- """format_infraction_with_duration should work for None expiry."""
+ def test_format_with_duration_none_expiry(self):
+ """format_with_duration should work for None expiry."""
test_cases = (
(None, None, None, None),
@@ -67,10 +59,10 @@ class TimeTests(unittest.TestCase):
for expiry, date_from, max_units, expected in test_cases:
with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected):
- self.assertEqual(time.format_infraction_with_duration(expiry, date_from, max_units), expected)
+ self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected)
- def test_format_infraction_with_duration_custom_units(self):
- """format_infraction_with_duration should work for custom max_units."""
+ def test_format_with_duration_custom_units(self):
+ """format_with_duration should work for custom max_units."""
test_cases = (
('3000-12-12T00:01:00Z', datetime(3000, 12, 11, 12, 5, 5, tzinfo=timezone.utc), 6,
'<t:32533488060:f> (11 hours, 55 minutes and 55 seconds)'),
@@ -80,10 +72,10 @@ class TimeTests(unittest.TestCase):
for expiry, date_from, max_units, expected in test_cases:
with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected):
- self.assertEqual(time.format_infraction_with_duration(expiry, date_from, max_units), expected)
+ self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected)
- def test_format_infraction_with_duration_normal_usage(self):
- """format_infraction_with_duration should work for normal usage, across various durations."""
+ def test_format_with_duration_normal_usage(self):
+ """format_with_duration should work for normal usage, across various durations."""
utc = timezone.utc
test_cases = (
('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5, tzinfo=utc), 2,
@@ -105,11 +97,11 @@ class TimeTests(unittest.TestCase):
for expiry, date_from, max_units, expected in test_cases:
with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected):
- self.assertEqual(time.format_infraction_with_duration(expiry, date_from, max_units), expected)
+ self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected)
def test_until_expiration_with_duration_none_expiry(self):
- """until_expiration should work for None expiry."""
- self.assertEqual(time.until_expiration(None), None)
+ """until_expiration should return "Permanent" is expiry is None."""
+ self.assertEqual(time.until_expiration(None), "Permanent")
def test_until_expiration_with_duration_custom_units(self):
"""until_expiration should work for custom max_units."""
@@ -130,7 +122,6 @@ class TimeTests(unittest.TestCase):
('3000-12-12T00:00:00Z', '<t:32533488000:R>'),
('3000-11-23T20:09:00Z', '<t:32531918940:R>'),
('3000-11-23T20:09:00Z', '<t:32531918940:R>'),
- (None, None),
)
for expiry, expected in test_cases:
diff --git a/tests/helpers.py b/tests/helpers.py
index 9d4988d23..bd1418ab9 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -7,9 +7,9 @@ import unittest.mock
from asyncio import AbstractEventLoop
from typing import Iterable, Optional
-import discord
+import disnake
from aiohttp import ClientSession
-from discord.ext.commands import Context
+from disnake.ext.commands import Context
from bot.api import APIClient
from bot.async_stats import AsyncStatsClient
@@ -26,11 +26,11 @@ for logger in logging.Logger.manager.loggerDict.values():
logger.setLevel(logging.CRITICAL)
-class HashableMixin(discord.mixins.EqualityComparable):
+class HashableMixin(disnake.mixins.EqualityComparable):
"""
- Mixin that provides similar hashing and equality functionality as discord.py's `Hashable` mixin.
+ Mixin that provides similar hashing and equality functionality as disnake's `Hashable` mixin.
- Note: discord.py`s `Hashable` mixin bit-shifts `self.id` (`>> 22`); to prevent hash-collisions
+ Note: disnake`s `Hashable` mixin bit-shifts `self.id` (`>> 22`); to prevent hash-collisions
for the relative small `id` integers we generally use in tests, this bit-shift is omitted.
"""
@@ -39,22 +39,22 @@ class HashableMixin(discord.mixins.EqualityComparable):
class ColourMixin:
- """A mixin for Mocks that provides the aliasing of (accent_)color->(accent_)colour like discord.py does."""
+ """A mixin for Mocks that provides the aliasing of (accent_)color->(accent_)colour like disnake does."""
@property
- def color(self) -> discord.Colour:
+ def color(self) -> disnake.Colour:
return self.colour
@color.setter
- def color(self, color: discord.Colour) -> None:
+ def color(self, color: disnake.Colour) -> None:
self.colour = color
@property
- def accent_color(self) -> discord.Colour:
+ def accent_color(self) -> disnake.Colour:
return self.accent_colour
@accent_color.setter
- def accent_color(self, color: discord.Colour) -> None:
+ def accent_color(self, color: disnake.Colour) -> None:
self.accent_colour = color
@@ -63,7 +63,7 @@ class CustomMockMixin:
Provides common functionality for our custom Mock types.
The `_get_child_mock` method automatically returns an AsyncMock for coroutine methods of the mock
- object. As discord.py also uses synchronous methods that nonetheless return coroutine objects, the
+ object. As disnake also uses synchronous methods that nonetheless return coroutine objects, the
class attribute `additional_spec_asyncs` can be overwritten with an iterable containing additional
attribute names that should also mocked with an AsyncMock instead of a regular MagicMock/Mock. The
class method `spec_set` can be overwritten with the object that should be uses as the specification
@@ -119,7 +119,7 @@ class CustomMockMixin:
return klass(**kw)
-# Create a guild instance to get a realistic Mock of `discord.Guild`
+# Create a guild instance to get a realistic Mock of `disnake.Guild`
guild_data = {
'id': 1,
'name': 'guild',
@@ -139,20 +139,20 @@ guild_data = {
'owner_id': 1,
'afk_channel_id': 464033278631084042,
}
-guild_instance = discord.Guild(data=guild_data, state=unittest.mock.MagicMock())
+guild_instance = disnake.Guild(data=guild_data, state=unittest.mock.MagicMock())
class MockGuild(CustomMockMixin, unittest.mock.Mock, HashableMixin):
"""
- A `Mock` subclass to mock `discord.Guild` objects.
+ A `Mock` subclass to mock `disnake.Guild` objects.
- A MockGuild instance will follow the specifications of a `discord.Guild` instance. This means
+ A MockGuild instance will follow the specifications of a `disnake.Guild` instance. This means
that if the code you're testing tries to access an attribute or method that normally does not
- exist for a `discord.Guild` object this will raise an `AttributeError`. This is to make sure our
- tests fail if the code we're testing uses a `discord.Guild` object in the wrong way.
+ exist for a `disnake.Guild` object this will raise an `AttributeError`. This is to make sure our
+ tests fail if the code we're testing uses a `disnake.Guild` object in the wrong way.
One restriction of that is that if the code tries to access an attribute that normally does not
- exist for `discord.Guild` instance but was added dynamically, this will raise an exception with
+ exist for `disnake.Guild` instance but was added dynamically, this will raise an exception with
the mocked object. To get around that, you can set the non-standard attribute explicitly for the
instance of `MockGuild`:
@@ -160,10 +160,10 @@ class MockGuild(CustomMockMixin, unittest.mock.Mock, HashableMixin):
>>> guild.attribute_that_normally_does_not_exist = unittest.mock.MagicMock()
In addition to attribute simulation, mocked guild object will pass an `isinstance` check against
- `discord.Guild`:
+ `disnake.Guild`:
>>> guild = MockGuild()
- >>> isinstance(guild, discord.Guild)
+ >>> isinstance(guild, disnake.Guild)
True
For more info, see the `Mocking` section in `tests/README.md`.
@@ -179,16 +179,16 @@ class MockGuild(CustomMockMixin, unittest.mock.Mock, HashableMixin):
self.roles.extend(roles)
-# Create a Role instance to get a realistic Mock of `discord.Role`
+# Create a Role instance to get a realistic Mock of `disnake.Role`
role_data = {'name': 'role', 'id': 1}
-role_instance = discord.Role(guild=guild_instance, state=unittest.mock.MagicMock(), data=role_data)
+role_instance = disnake.Role(guild=guild_instance, state=unittest.mock.MagicMock(), data=role_data)
class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin):
"""
- A Mock subclass to mock `discord.Role` objects.
+ A Mock subclass to mock `disnake.Role` objects.
- Instances of this class will follow the specifications of `discord.Role` instances. For more
+ Instances of this class will follow the specifications of `disnake.Role` instances. For more
information, see the `MockGuild` docstring.
"""
spec_set = role_instance
@@ -198,40 +198,40 @@ class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin):
'id': next(self.discord_id),
'name': 'role',
'position': 1,
- 'colour': discord.Colour(0xdeadbf),
- 'permissions': discord.Permissions(),
+ 'colour': disnake.Colour(0xdeadbf),
+ 'permissions': disnake.Permissions(),
}
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
if isinstance(self.colour, int):
- self.colour = discord.Colour(self.colour)
+ self.colour = disnake.Colour(self.colour)
if isinstance(self.permissions, int):
- self.permissions = discord.Permissions(self.permissions)
+ self.permissions = disnake.Permissions(self.permissions)
if 'mention' not in kwargs:
self.mention = f'&{self.name}'
def __lt__(self, other):
- """Simplified position-based comparisons similar to those of `discord.Role`."""
+ """Simplified position-based comparisons similar to those of `disnake.Role`."""
return self.position < other.position
def __ge__(self, other):
- """Simplified position-based comparisons similar to those of `discord.Role`."""
+ """Simplified position-based comparisons similar to those of `disnake.Role`."""
return self.position >= other.position
-# Create a Member instance to get a realistic Mock of `discord.Member`
+# Create a Member instance to get a realistic Mock of `disnake.Member`
member_data = {'user': 'lemon', 'roles': [1]}
state_mock = unittest.mock.MagicMock()
-member_instance = discord.Member(data=member_data, guild=guild_instance, state=state_mock)
+member_instance = disnake.Member(data=member_data, guild=guild_instance, state=state_mock)
class MockMember(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin):
"""
A Mock subclass to mock Member objects.
- Instances of this class will follow the specifications of `discord.Member` instances. For more
+ Instances of this class will follow the specifications of `disnake.Member` instances. For more
information, see the `MockGuild` docstring.
"""
spec_set = member_instance
@@ -249,11 +249,11 @@ class MockMember(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin
self.mention = f"@{self.name}"
-# Create a User instance to get a realistic Mock of `discord.User`
+# Create a User instance to get a realistic Mock of `disnake.User`
_user_data_mock = collections.defaultdict(unittest.mock.MagicMock, {
"accent_color": 0
})
-user_instance = discord.User(
+user_instance = disnake.User(
data=unittest.mock.MagicMock(get=unittest.mock.Mock(side_effect=_user_data_mock.get)),
state=unittest.mock.MagicMock()
)
@@ -263,7 +263,7 @@ class MockUser(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin):
"""
A Mock subclass to mock User objects.
- Instances of this class will follow the specifications of `discord.User` instances. For more
+ Instances of this class will follow the specifications of `disnake.User` instances. For more
information, see the `MockGuild` docstring.
"""
spec_set = user_instance
@@ -305,7 +305,7 @@ class MockBot(CustomMockMixin, unittest.mock.MagicMock):
"""
A MagicMock subclass to mock Bot objects.
- Instances of this class will follow the specifications of `discord.ext.commands.Bot` instances.
+ Instances of this class will follow the specifications of `disnake.ext.commands.Bot` instances.
For more information, see the `MockGuild` docstring.
"""
spec_set = Bot(
@@ -324,7 +324,7 @@ class MockBot(CustomMockMixin, unittest.mock.MagicMock):
self.stats = unittest.mock.create_autospec(spec=AsyncStatsClient, spec_set=True)
-# Create a TextChannel instance to get a realistic MagicMock of `discord.TextChannel`
+# Create a TextChannel instance to get a realistic MagicMock of `disnake.TextChannel`
channel_data = {
'id': 1,
'type': 'TextChannel',
@@ -337,17 +337,17 @@ channel_data = {
}
state = unittest.mock.MagicMock()
guild = unittest.mock.MagicMock()
-text_channel_instance = discord.TextChannel(state=state, guild=guild, data=channel_data)
+text_channel_instance = disnake.TextChannel(state=state, guild=guild, data=channel_data)
channel_data["type"] = "VoiceChannel"
-voice_channel_instance = discord.VoiceChannel(state=state, guild=guild, data=channel_data)
+voice_channel_instance = disnake.VoiceChannel(state=state, guild=guild, data=channel_data)
class MockTextChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin):
"""
A MagicMock subclass to mock TextChannel objects.
- Instances of this class will follow the specifications of `discord.TextChannel` instances. For
+ Instances of this class will follow the specifications of `disnake.TextChannel` instances. For
more information, see the `MockGuild` docstring.
"""
spec_set = text_channel_instance
@@ -364,7 +364,7 @@ class MockVoiceChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin):
"""
A MagicMock subclass to mock VoiceChannel objects.
- Instances of this class will follow the specifications of `discord.VoiceChannel` instances. For
+ Instances of this class will follow the specifications of `disnake.VoiceChannel` instances. For
more information, see the `MockGuild` docstring.
"""
spec_set = voice_channel_instance
@@ -381,14 +381,14 @@ class MockVoiceChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin):
state = unittest.mock.MagicMock()
me = unittest.mock.MagicMock()
dm_channel_data = {"id": 1, "recipients": [unittest.mock.MagicMock()]}
-dm_channel_instance = discord.DMChannel(me=me, state=state, data=dm_channel_data)
+dm_channel_instance = disnake.DMChannel(me=me, state=state, data=dm_channel_data)
class MockDMChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin):
"""
A MagicMock subclass to mock TextChannel objects.
- Instances of this class will follow the specifications of `discord.TextChannel` instances. For
+ Instances of this class will follow the specifications of `disnake.TextChannel` instances. For
more information, see the `MockGuild` docstring.
"""
spec_set = dm_channel_instance
@@ -398,17 +398,17 @@ class MockDMChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin):
super().__init__(**collections.ChainMap(kwargs, default_kwargs))
-# Create CategoryChannel instance to get a realistic MagicMock of `discord.CategoryChannel`
+# Create CategoryChannel instance to get a realistic MagicMock of `disnake.CategoryChannel`
category_channel_data = {
'id': 1,
- 'type': discord.ChannelType.category,
+ 'type': disnake.ChannelType.category,
'name': 'category',
'position': 1,
}
state = unittest.mock.MagicMock()
guild = unittest.mock.MagicMock()
-category_channel_instance = discord.CategoryChannel(
+category_channel_instance = disnake.CategoryChannel(
state=state, guild=guild, data=category_channel_data
)
@@ -419,7 +419,7 @@ class MockCategoryChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin):
super().__init__(**collections.ChainMap(default_kwargs, kwargs))
-# Create a Message instance to get a realistic MagicMock of `discord.Message`
+# Create a Message instance to get a realistic MagicMock of `disnake.Message`
message_data = {
'id': 1,
'webhook_id': 431341013479718912,
@@ -438,10 +438,10 @@ message_data = {
}
state = unittest.mock.MagicMock()
channel = unittest.mock.MagicMock()
-message_instance = discord.Message(state=state, channel=channel, data=message_data)
+message_instance = disnake.Message(state=state, channel=channel, data=message_data)
-# Create a Context instance to get a realistic MagicMock of `discord.ext.commands.Context`
+# Create a Context instance to get a realistic MagicMock of `disnake.ext.commands.Context`
context_instance = Context(
message=unittest.mock.MagicMock(),
prefix="$",
@@ -455,7 +455,7 @@ class MockContext(CustomMockMixin, unittest.mock.MagicMock):
"""
A MagicMock subclass to mock Context objects.
- Instances of this class will follow the specifications of `discord.ext.commands.Context`
+ Instances of this class will follow the specifications of `disnake.ext.commands.Context`
instances. For more information, see the `MockGuild` docstring.
"""
spec_set = context_instance
@@ -471,14 +471,14 @@ class MockContext(CustomMockMixin, unittest.mock.MagicMock):
self.invoked_from_error_handler = kwargs.get('invoked_from_error_handler', False)
-attachment_instance = discord.Attachment(data=unittest.mock.MagicMock(id=1), state=unittest.mock.MagicMock())
+attachment_instance = disnake.Attachment(data=unittest.mock.MagicMock(id=1), state=unittest.mock.MagicMock())
class MockAttachment(CustomMockMixin, unittest.mock.MagicMock):
"""
A MagicMock subclass to mock Attachment objects.
- Instances of this class will follow the specifications of `discord.Attachment` instances. For
+ Instances of this class will follow the specifications of `disnake.Attachment` instances. For
more information, see the `MockGuild` docstring.
"""
spec_set = attachment_instance
@@ -488,7 +488,7 @@ class MockMessage(CustomMockMixin, unittest.mock.MagicMock):
"""
A MagicMock subclass to mock Message objects.
- Instances of this class will follow the specifications of `discord.Message` instances. For more
+ Instances of this class will follow the specifications of `disnake.Message` instances. For more
information, see the `MockGuild` docstring.
"""
spec_set = message_instance
@@ -501,14 +501,14 @@ class MockMessage(CustomMockMixin, unittest.mock.MagicMock):
emoji_data = {'require_colons': True, 'managed': True, 'id': 1, 'name': 'hyperlemon'}
-emoji_instance = discord.Emoji(guild=MockGuild(), state=unittest.mock.MagicMock(), data=emoji_data)
+emoji_instance = disnake.Emoji(guild=MockGuild(), state=unittest.mock.MagicMock(), data=emoji_data)
class MockEmoji(CustomMockMixin, unittest.mock.MagicMock):
"""
A MagicMock subclass to mock Emoji objects.
- Instances of this class will follow the specifications of `discord.Emoji` instances. For more
+ Instances of this class will follow the specifications of `disnake.Emoji` instances. For more
information, see the `MockGuild` docstring.
"""
spec_set = emoji_instance
@@ -518,27 +518,27 @@ class MockEmoji(CustomMockMixin, unittest.mock.MagicMock):
self.guild = kwargs.get('guild', MockGuild())
-partial_emoji_instance = discord.PartialEmoji(animated=False, name='guido')
+partial_emoji_instance = disnake.PartialEmoji(animated=False, name='guido')
class MockPartialEmoji(CustomMockMixin, unittest.mock.MagicMock):
"""
A MagicMock subclass to mock PartialEmoji objects.
- Instances of this class will follow the specifications of `discord.PartialEmoji` instances. For
+ Instances of this class will follow the specifications of `disnake.PartialEmoji` instances. For
more information, see the `MockGuild` docstring.
"""
spec_set = partial_emoji_instance
-reaction_instance = discord.Reaction(message=MockMessage(), data={'me': True}, emoji=MockEmoji())
+reaction_instance = disnake.Reaction(message=MockMessage(), data={'me': True}, emoji=MockEmoji())
class MockReaction(CustomMockMixin, unittest.mock.MagicMock):
"""
A MagicMock subclass to mock Reaction objects.
- Instances of this class will follow the specifications of `discord.Reaction` instances. For
+ Instances of this class will follow the specifications of `disnake.Reaction` instances. For
more information, see the `MockGuild` docstring.
"""
spec_set = reaction_instance
@@ -556,14 +556,14 @@ class MockReaction(CustomMockMixin, unittest.mock.MagicMock):
self.__str__.return_value = str(self.emoji)
-webhook_instance = discord.Webhook(data=unittest.mock.MagicMock(), session=unittest.mock.MagicMock())
+webhook_instance = disnake.Webhook(data=unittest.mock.MagicMock(), session=unittest.mock.MagicMock())
class MockAsyncWebhook(CustomMockMixin, unittest.mock.MagicMock):
"""
A MagicMock subclass to mock Webhook objects using an AsyncWebhookAdapter.
- Instances of this class will follow the specifications of `discord.Webhook` instances. For
+ Instances of this class will follow the specifications of `disnake.Webhook` instances. For
more information, see the `MockGuild` docstring.
"""
spec_set = webhook_instance
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 81285e009..c5e799a85 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -2,20 +2,20 @@ import asyncio
import unittest
import unittest.mock
-import discord
+import disnake
from tests import helpers
class DiscordMocksTests(unittest.TestCase):
- """Tests for our specialized discord.py mocks."""
+ """Tests for our specialized disnake mocks."""
def test_mock_role_default_initialization(self):
"""Test if the default initialization of MockRole results in the correct object."""
role = helpers.MockRole()
- # The `spec` argument makes sure `isistance` checks with `discord.Role` pass
- self.assertIsInstance(role, discord.Role)
+ # The `spec` argument makes sure `isistance` checks with `disnake.Role` pass
+ self.assertIsInstance(role, disnake.Role)
self.assertEqual(role.name, "role")
self.assertEqual(role.position, 1)
@@ -61,8 +61,8 @@ class DiscordMocksTests(unittest.TestCase):
"""Test if the default initialization of Mockmember results in the correct object."""
member = helpers.MockMember()
- # The `spec` argument makes sure `isistance` checks with `discord.Member` pass
- self.assertIsInstance(member, discord.Member)
+ # The `spec` argument makes sure `isistance` checks with `disnake.Member` pass
+ self.assertIsInstance(member, disnake.Member)
self.assertEqual(member.name, "member")
self.assertListEqual(member.roles, [helpers.MockRole(name="@everyone", position=1, id=0)])
@@ -86,18 +86,18 @@ class DiscordMocksTests(unittest.TestCase):
"""Test if MockMember accepts and sets abitrary keyword arguments."""
member = helpers.MockMember(
nick="Dino Man",
- colour=discord.Colour.default(),
+ colour=disnake.Colour.default(),
)
self.assertEqual(member.nick, "Dino Man")
- self.assertEqual(member.colour, discord.Colour.default())
+ self.assertEqual(member.colour, disnake.Colour.default())
def test_mock_guild_default_initialization(self):
"""Test if the default initialization of Mockguild results in the correct object."""
guild = helpers.MockGuild()
- # The `spec` argument makes sure `isistance` checks with `discord.Guild` pass
- self.assertIsInstance(guild, discord.Guild)
+ # The `spec` argument makes sure `isistance` checks with `disnake.Guild` pass
+ self.assertIsInstance(guild, disnake.Guild)
self.assertListEqual(guild.roles, [helpers.MockRole(name="@everyone", position=1, id=0)])
self.assertListEqual(guild.members, [])
@@ -127,15 +127,15 @@ class DiscordMocksTests(unittest.TestCase):
"""Tests if MockBot initializes with the correct values."""
bot = helpers.MockBot()
- # The `spec` argument makes sure `isistance` checks with `discord.ext.commands.Bot` pass
- self.assertIsInstance(bot, discord.ext.commands.Bot)
+ # The `spec` argument makes sure `isistance` checks with `disnake.ext.commands.Bot` pass
+ self.assertIsInstance(bot, disnake.ext.commands.Bot)
def test_mock_context_default_initialization(self):
"""Tests if MockContext initializes with the correct values."""
context = helpers.MockContext()
- # The `spec` argument makes sure `isistance` checks with `discord.ext.commands.Context` pass
- self.assertIsInstance(context, discord.ext.commands.Context)
+ # The `spec` argument makes sure `isistance` checks with `disnake.ext.commands.Context` pass
+ self.assertIsInstance(context, disnake.ext.commands.Context)
self.assertIsInstance(context.bot, helpers.MockBot)
self.assertIsInstance(context.guild, helpers.MockGuild)