aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/bot/exts/backend/test_error_handler.py62
-rw-r--r--tests/bot/exts/info/test_information.py76
-rw-r--r--tests/bot/exts/moderation/infraction/test_infractions.py175
-rw-r--r--tests/bot/exts/moderation/infraction/test_utils.py36
-rw-r--r--tests/bot/exts/moderation/test_clean.py104
-rw-r--r--tests/bot/exts/moderation/test_incidents.py94
-rw-r--r--tests/bot/exts/utils/test_snekbox.py183
-rw-r--r--tests/bot/test_converters.py17
-rw-r--r--tests/bot/utils/test_time.py47
9 files changed, 537 insertions, 257 deletions
diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py
index 462f718e6..35fa0ee59 100644
--- a/tests/bot/exts/backend/test_error_handler.py
+++ b/tests/bot/exts/backend/test_error_handler.py
@@ -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/info/test_information.py b/tests/bot/exts/info/test_information.py
index 632287322..d896b7652 100644
--- a/tests/bot/exts/info/test_information.py
+++ b/tests/bot/exts/info/test_information.py
@@ -1,6 +1,7 @@
import textwrap
import unittest
import unittest.mock
+from datetime import datetime
import discord
@@ -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,7 +446,8 @@ 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))
@@ -417,12 +455,17 @@ 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_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.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())
@@ -430,13 +473,18 @@ 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_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..052048053 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 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..ff81ddd65 100644
--- a/tests/bot/exts/moderation/infraction/test_utils.py
+++ b/tests/bot/exts/moderation/infraction/test_utils.py
@@ -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 ccc842050..cfe0c4b03 100644
--- a/tests/bot/exts/moderation/test_incidents.py
+++ b/tests/bot/exts/moderation/test_incidents.py
@@ -3,13 +3,16 @@ import enum
import logging
import typing as t
import unittest
+from unittest import mock
from unittest.mock import AsyncMock, MagicMock, Mock, call, patch
import aiohttp
import discord
+from async_rediscache import RedisSession
from bot.constants import Colours
from bot.exts.moderation import incidents
+from bot.utils.messages import format_user
from tests.helpers import (
MockAsyncWebhook, MockAttachment, MockBot, MockMember, MockMessage, MockReaction, MockRole, MockTextChannel,
MockUser
@@ -276,6 +279,22 @@ class TestIncidents(unittest.IsolatedAsyncioTestCase):
the instance as they wish.
"""
+ session = None
+
+ async def flush(self):
+ """Flush everything from the database to prevent carry-overs between tests."""
+ with await self.session.pool as connection:
+ await connection.flushall()
+
+ async def asyncSetUp(self): # noqa: N802
+ self.session = RedisSession(use_fakeredis=True)
+ await self.session.connect()
+ await self.flush()
+
+ async def asyncTearDown(self): # noqa: N802
+ if self.session:
+ await self.session.close()
+
def setUp(self):
"""
Prepare a fresh `Incidents` instance for each test.
@@ -506,7 +525,7 @@ class TestProcessEvent(TestIncidents):
with patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", mock_task):
await self.cog_instance.process_event(
reaction=incidents.Signal.ACTIONED.value,
- incident=MockMessage(),
+ incident=MockMessage(id=123),
member=MockMember(roles=[MockRole(id=1)])
)
@@ -526,7 +545,7 @@ class TestProcessEvent(TestIncidents):
with patch("bot.exts.moderation.incidents.Incidents.make_confirmation_task", mock_task):
await self.cog_instance.process_event(
reaction=incidents.Signal.ACTIONED.value,
- incident=MockMessage(),
+ incident=MockMessage(id=123),
member=MockMember(roles=[MockRole(id=1)])
)
except asyncio.TimeoutError:
@@ -761,3 +780,74 @@ class TestOnMessage(TestIncidents):
await self.cog_instance.on_message(MockMessage())
mock_add_signals.assert_not_called()
+
+
+class TestMessageLinkEmbeds(TestIncidents):
+ """Tests for `extract_message_links` coroutine."""
+
+ async def test_shorten_text(self):
+ """Test all cases of text shortening by mocking messages."""
+ tests = {
+ "thisisasingleword"*10: "thisisasinglewordthisisasinglewordthisisasinglewor...",
+
+ "\n".join("Lets make a new line test".split()): "Lets\nmake\na...",
+
+ 'Hello, World!' * 300: (
+ "Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!"
+ "Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!"
+ "Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!Hello, World!"
+ "Hello, World!Hello, World!H..."
+ )
+ }
+
+ for content, expected_conversion in tests.items():
+ with self.subTest(content=content, expected_conversion=expected_conversion):
+ conversion = incidents.shorten_text(content)
+ self.assertEqual(conversion, expected_conversion)
+
+ async def extract_and_form_message_link_embeds(self):
+ """
+ Extract message links from a mocked message and form the message link embed.
+
+ Considers all types of message links, discord supports.
+ """
+ self.guild_id_patcher = mock.patch("bot.exts.backend.sync._cog.constants.Guild.id", 5)
+ self.guild_id = self.guild_id_patcher.start()
+
+ msg = MockMessage(id=555, content="Hello, World!" * 3000)
+ msg.channel.mention = "#lemonade-stand"
+
+ msg_links = [
+ # Valid Message links
+ f"https://discord.com/channels/{self.guild_id}/{msg.channel.discord_id}/{msg.discord_id}",
+ f"http://canary.discord.com/channels/{self.guild_id}/{msg.channel.discord_id}/{msg.discord_id}",
+
+ # Invalid Message links
+ f"https://discord.com/channels/{msg.channel.discord_id}/{msg.discord_id}",
+ f"https://discord.com/channels/{self.guild_id}/{msg.channel.discord_id}000/{msg.discord_id}",
+ ]
+
+ incident_msg = MockMessage(
+ id=777,
+ content=(
+ f"I would like to report the following messages, "
+ f"as they break our rules: \n{', '.join(msg_links)}"
+ )
+ )
+
+ with patch(
+ "bot.exts.moderation.incidents.Incidents.extract_message_links", AsyncMock()
+ ) as mock_extract_message_links:
+ embeds = mock_extract_message_links(incident_msg)
+ description = (
+ f"**Author:** {format_user(msg.author)}\n"
+ f"**Channel:** {msg.channel.mention} ({msg.channel.category}/#{msg.channel.name})\n"
+ f"**Content:** {('Hello, World!' * 3000)[:300] + '...'}\n"
+ )
+
+ # Check number of embeds returned with number of valid links
+ self.assertEqual(len(embeds), 2)
+
+ # Check for the embed descriptions
+ for embed in embeds:
+ self.assertEqual(embed.description, description)
diff --git a/tests/bot/exts/utils/test_snekbox.py b/tests/bot/exts/utils/test_snekbox.py
index 321a92445..f68a20089 100644
--- a/tests/bot/exts/utils/test_snekbox.py
+++ b/tests/bot/exts/utils/test_snekbox.py
@@ -2,6 +2,7 @@ import asyncio
import unittest
from unittest.mock import AsyncMock, MagicMock, Mock, call, create_autospec, patch
+from discord import AllowedMentions
from discord.ext import commands
from bot import constants
@@ -16,7 +17,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
self.bot = MockBot()
self.cog = Snekbox(bot=self.bot)
- async def test_post_eval(self):
+ async def test_post_job(self):
"""Post the eval code to the URLs.snekbox_eval_api endpoint."""
resp = MagicMock()
resp.json = AsyncMock(return_value="return")
@@ -25,7 +26,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
context_manager.__aenter__.return_value = resp
self.bot.http_session.post.return_value = context_manager
- self.assertEqual(await self.cog.post_eval("import random"), "return")
+ self.assertEqual(await self.cog.post_job("import random"), "return")
self.bot.http_session.post.assert_called_with(
constants.URLs.snekbox_eval_api,
json={"input": "import random"},
@@ -44,7 +45,8 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
await self.cog.upload_output("Test output.")
mock_paste_util.assert_called_once_with("Test output.", extension="txt")
- def test_prepare_input(self):
+ async def test_codeblock_converter(self):
+ ctx = MockContext()
cases = (
('print("Hello world!")', 'print("Hello world!")', 'non-formatted'),
('`print("Hello world!")`', 'print("Hello world!")', 'one line code block'),
@@ -60,7 +62,24 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
)
for case, expected, testname in cases:
with self.subTest(msg=f'Extract code from {testname}.'):
- self.assertEqual(self.cog.prepare_input(case), expected)
+ self.assertEqual(
+ '\n'.join(await snekbox.CodeblockConverter.convert(ctx, case)), expected
+ )
+
+ def test_prepare_timeit_input(self):
+ """Test the prepare_timeit_input codeblock detection."""
+ base_args = ('-m', 'timeit', '-s')
+ cases = (
+ (['print("Hello World")'], '', 'single block of code'),
+ (['x = 1', 'print(x)'], 'x = 1', 'two blocks of code'),
+ (['x = 1', 'print(x)', 'print("Some other code.")'], 'x = 1', 'three blocks of code')
+ )
+
+ for case, setup_code, testname in cases:
+ setup = snekbox.TIMEIT_SETUP_WRAPPER.format(setup=setup_code)
+ expected = ('\n'.join(case[1:] if setup_code else case), [*base_args, setup])
+ with self.subTest(msg=f'Test with {testname} and expected return {expected}'):
+ self.assertEqual(self.cog.prepare_timeit_input(case), expected)
def test_get_results_message(self):
"""Return error and message according to the eval result."""
@@ -71,13 +90,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
)
for stdout, returncode, expected in cases:
with self.subTest(stdout=stdout, returncode=returncode, expected=expected):
- actual = self.cog.get_results_message({'stdout': stdout, 'returncode': returncode})
+ actual = self.cog.get_results_message({'stdout': stdout, 'returncode': returncode}, 'eval')
self.assertEqual(actual, expected)
@patch('bot.exts.utils.snekbox.Signals', side_effect=ValueError)
def test_get_results_message_invalid_signal(self, mock_signals: Mock):
self.assertEqual(
- self.cog.get_results_message({'stdout': '', 'returncode': 127}),
+ self.cog.get_results_message({'stdout': '', 'returncode': 127}, 'eval'),
('Your eval job has completed with return code 127', '')
)
@@ -85,7 +104,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
def test_get_results_message_valid_signal(self, mock_signals: Mock):
mock_signals.return_value.name = 'SIGTEST'
self.assertEqual(
- self.cog.get_results_message({'stdout': '', 'returncode': 127}),
+ self.cog.get_results_message({'stdout': '', 'returncode': 127}, 'eval'),
('Your eval job has completed with return code 127 (SIGTEST)', '')
)
@@ -155,28 +174,29 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
"""Test the eval command procedure."""
ctx = MockContext()
response = MockMessage()
- self.cog.prepare_input = MagicMock(return_value='MyAwesomeFormattedCode')
- self.cog.send_eval = AsyncMock(return_value=response)
- self.cog.continue_eval = AsyncMock(return_value=None)
+ ctx.command = MagicMock()
- await self.cog.eval_command(self.cog, ctx=ctx, code='MyAwesomeCode')
- self.cog.prepare_input.assert_called_once_with('MyAwesomeCode')
- self.cog.send_eval.assert_called_once_with(ctx, 'MyAwesomeFormattedCode')
- self.cog.continue_eval.assert_called_once_with(ctx, response)
+ self.cog.send_job = AsyncMock(return_value=response)
+ self.cog.continue_job = AsyncMock(return_value=(None, None))
+
+ await self.cog.eval_command(self.cog, ctx=ctx, code=['MyAwesomeCode'])
+ self.cog.send_job.assert_called_once_with(ctx, 'MyAwesomeCode', args=None, job_name='eval')
+ self.cog.continue_job.assert_called_once_with(ctx, response, ctx.command)
async def test_eval_command_evaluate_twice(self):
"""Test the eval and re-eval command procedure."""
ctx = MockContext()
response = MockMessage()
- self.cog.prepare_input = MagicMock(return_value='MyAwesomeFormattedCode')
- self.cog.send_eval = AsyncMock(return_value=response)
- self.cog.continue_eval = AsyncMock()
- self.cog.continue_eval.side_effect = ('MyAwesomeCode-2', None)
-
- await self.cog.eval_command(self.cog, ctx=ctx, code='MyAwesomeCode')
- self.cog.prepare_input.has_calls(call('MyAwesomeCode'), call('MyAwesomeCode-2'))
- self.cog.send_eval.assert_called_with(ctx, 'MyAwesomeFormattedCode')
- self.cog.continue_eval.assert_called_with(ctx, response)
+ ctx.command = MagicMock()
+ self.cog.send_job = AsyncMock(return_value=response)
+ self.cog.continue_job = AsyncMock()
+ self.cog.continue_job.side_effect = (('MyAwesomeFormattedCode', None), (None, None))
+
+ await self.cog.eval_command(self.cog, ctx=ctx, code=['MyAwesomeCode'])
+ self.cog.send_job.assert_called_with(
+ ctx, 'MyAwesomeFormattedCode', args=None, job_name='eval'
+ )
+ self.cog.continue_job.assert_called_with(ctx, response, ctx.command)
async def test_eval_command_reject_two_eval_at_the_same_time(self):
"""Test if the eval command rejects an eval if the author already have a running eval."""
@@ -190,90 +210,99 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
"@LemonLemonishBeard#0042 You've already got a job running - please wait for it to finish!"
)
- async def test_eval_command_call_help(self):
- """Test if the eval command call the help command if no code is provided."""
- ctx = MockContext(command="sentinel")
- await self.cog.eval_command(self.cog, ctx=ctx, code='')
- ctx.send_help.assert_called_once_with(ctx.command)
-
- async def test_send_eval(self):
- """Test the send_eval function."""
+ async def test_send_job(self):
+ """Test the send_job function."""
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.post_job = AsyncMock(return_value={'stdout': '', 'returncode': 0})
self.cog.get_results_message = MagicMock(return_value=('Return code 0', ''))
self.cog.get_status_emoji = MagicMock(return_value=':yay!:')
self.cog.format_output = AsyncMock(return_value=('[No output]', None))
mocked_filter_cog = MagicMock()
- mocked_filter_cog.filter_eval = AsyncMock(return_value=False)
+ mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=False)
self.bot.get_cog.return_value = mocked_filter_cog
- await self.cog.send_eval(ctx, 'MyAwesomeCode')
- ctx.send.assert_called_once_with(
+ await self.cog.send_job(ctx, 'MyAwesomeCode', job_name='eval')
+
+ 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```'
)
- self.cog.post_eval.assert_called_once_with('MyAwesomeCode')
+ 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_job.assert_called_once_with('MyAwesomeCode', args=None)
self.cog.get_status_emoji.assert_called_once_with({'stdout': '', 'returncode': 0})
- self.cog.get_results_message.assert_called_once_with({'stdout': '', 'returncode': 0})
+ self.cog.get_results_message.assert_called_once_with({'stdout': '', 'returncode': 0}, 'eval')
self.cog.format_output.assert_called_once_with('')
- async def test_send_eval_with_paste_link(self):
- """Test the send_eval function with a too long output that generate a paste link."""
+ async def test_send_job_with_paste_link(self):
+ """Test the send_job function with a too long output that generate a paste link."""
ctx = MockContext()
ctx.message = MockMessage()
ctx.send = AsyncMock()
ctx.author.mention = '@LemonLemonishBeard#0042'
- self.cog.post_eval = AsyncMock(return_value={'stdout': 'Way too long beard', 'returncode': 0})
+ self.cog.post_job = AsyncMock(return_value={'stdout': 'Way too long beard', 'returncode': 0})
self.cog.get_results_message = MagicMock(return_value=('Return code 0', ''))
self.cog.get_status_emoji = MagicMock(return_value=':yay!:')
self.cog.format_output = AsyncMock(return_value=('Way too long beard', 'lookatmybeard.com'))
mocked_filter_cog = MagicMock()
- mocked_filter_cog.filter_eval = AsyncMock(return_value=False)
+ mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=False)
self.bot.get_cog.return_value = mocked_filter_cog
- await self.cog.send_eval(ctx, 'MyAwesomeCode')
- ctx.send.assert_called_once_with(
+ await self.cog.send_job(ctx, 'MyAwesomeCode', job_name='eval')
+
+ 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.post_job.assert_called_once_with('MyAwesomeCode', args=None)
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})
+ self.cog.get_results_message.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0}, 'eval')
self.cog.format_output.assert_called_once_with('Way too long beard')
- async def test_send_eval_with_non_zero_eval(self):
- """Test the send_eval function with a code returning a non-zero code."""
+ async def test_send_job_with_non_zero_eval(self):
+ """Test the send_job function with a code returning a non-zero code."""
ctx = MockContext()
ctx.message = MockMessage()
ctx.send = AsyncMock()
ctx.author.mention = '@LemonLemonishBeard#0042'
- self.cog.post_eval = AsyncMock(return_value={'stdout': 'ERROR', 'returncode': 127})
+ self.cog.post_job = AsyncMock(return_value={'stdout': 'ERROR', 'returncode': 127})
self.cog.get_results_message = MagicMock(return_value=('Return code 127', 'Beard got stuck in the eval'))
self.cog.get_status_emoji = MagicMock(return_value=':nope!:')
self.cog.format_output = AsyncMock() # This function isn't called
mocked_filter_cog = MagicMock()
- mocked_filter_cog.filter_eval = AsyncMock(return_value=False)
+ mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=False)
self.bot.get_cog.return_value = mocked_filter_cog
- await self.cog.send_eval(ctx, 'MyAwesomeCode')
- ctx.send.assert_called_once_with(
+ await self.cog.send_job(ctx, 'MyAwesomeCode', job_name='eval')
+
+ 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.post_job.assert_called_once_with('MyAwesomeCode', args=None)
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})
+ self.cog.get_results_message.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}, 'eval')
self.cog.format_output.assert_not_called()
@patch("bot.exts.utils.snekbox.partial")
- async def test_continue_eval_does_continue(self, partial_mock):
- """Test that the continue_eval function does continue if required conditions are met."""
+ async def test_continue_job_does_continue(self, partial_mock):
+ """Test that the continue_job function does continue if required conditions are met."""
ctx = MockContext(message=MockMessage(add_reaction=AsyncMock(), clear_reactions=AsyncMock()))
response = MockMessage(delete=AsyncMock())
new_msg = MockMessage()
@@ -281,30 +310,30 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
expected = "NewCode"
self.cog.get_code = create_autospec(self.cog.get_code, spec_set=True, return_value=expected)
- actual = await self.cog.continue_eval(ctx, response)
- self.cog.get_code.assert_awaited_once_with(new_msg)
- self.assertEqual(actual, expected)
+ actual = await self.cog.continue_job(ctx, response, self.cog.eval_command)
+ self.cog.get_code.assert_awaited_once_with(new_msg, ctx.command)
+ self.assertEqual(actual, (expected, None))
self.bot.wait_for.assert_has_awaits(
(
call(
'message_edit',
- check=partial_mock(snekbox.predicate_eval_message_edit, ctx),
- timeout=snekbox.REEVAL_TIMEOUT,
+ check=partial_mock(snekbox.predicate_message_edit, ctx),
+ timeout=snekbox.REDO_TIMEOUT,
),
- call('reaction_add', check=partial_mock(snekbox.predicate_eval_emoji_reaction, ctx), timeout=10)
+ call('reaction_add', check=partial_mock(snekbox.predicate_emoji_reaction, ctx), timeout=10)
)
)
- ctx.message.add_reaction.assert_called_once_with(snekbox.REEVAL_EMOJI)
- ctx.message.clear_reaction.assert_called_once_with(snekbox.REEVAL_EMOJI)
+ ctx.message.add_reaction.assert_called_once_with(snekbox.REDO_EMOJI)
+ ctx.message.clear_reaction.assert_called_once_with(snekbox.REDO_EMOJI)
response.delete.assert_called_once()
- async def test_continue_eval_does_not_continue(self):
+ async def test_continue_job_does_not_continue(self):
ctx = MockContext(message=MockMessage(clear_reactions=AsyncMock()))
self.bot.wait_for.side_effect = asyncio.TimeoutError
- actual = await self.cog.continue_eval(ctx, MockMessage())
- self.assertEqual(actual, None)
- ctx.message.clear_reaction.assert_called_once_with(snekbox.REEVAL_EMOJI)
+ actual = await self.cog.continue_job(ctx, MockMessage(), self.cog.eval_command)
+ self.assertEqual(actual, (None, None))
+ ctx.message.clear_reaction.assert_called_once_with(snekbox.REDO_EMOJI)
async def test_get_code(self):
"""Should return 1st arg (or None) if eval cmd in message, otherwise return full content."""
@@ -327,13 +356,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
self.bot.get_context.return_value = MockContext(command=command)
message = MockMessage(content=content)
- actual_code = await self.cog.get_code(message)
+ actual_code = await self.cog.get_code(message, self.cog.eval_command)
self.bot.get_context.assert_awaited_once_with(message)
self.assertEqual(actual_code, expected_code)
- def test_predicate_eval_message_edit(self):
- """Test the predicate_eval_message_edit function."""
+ def test_predicate_message_edit(self):
+ """Test the predicate_message_edit function."""
msg0 = MockMessage(id=1, content='abc')
msg1 = MockMessage(id=2, content='abcdef')
msg2 = MockMessage(id=1, content='abcdef')
@@ -346,18 +375,18 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
for ctx_msg, new_msg, expected, testname in cases:
with self.subTest(msg=f'Messages with {testname} return {expected}'):
ctx = MockContext(message=ctx_msg)
- actual = snekbox.predicate_eval_message_edit(ctx, ctx_msg, new_msg)
+ actual = snekbox.predicate_message_edit(ctx, ctx_msg, new_msg)
self.assertEqual(actual, expected)
- def test_predicate_eval_emoji_reaction(self):
- """Test the predicate_eval_emoji_reaction function."""
+ def test_predicate_emoji_reaction(self):
+ """Test the predicate_emoji_reaction function."""
valid_reaction = MockReaction(message=MockMessage(id=1))
- valid_reaction.__str__.return_value = snekbox.REEVAL_EMOJI
+ valid_reaction.__str__.return_value = snekbox.REDO_EMOJI
valid_ctx = MockContext(message=MockMessage(id=1), author=MockUser(id=2))
valid_user = MockUser(id=2)
invalid_reaction_id = MockReaction(message=MockMessage(id=42))
- invalid_reaction_id.__str__.return_value = snekbox.REEVAL_EMOJI
+ invalid_reaction_id.__str__.return_value = snekbox.REDO_EMOJI
invalid_user_id = MockUser(id=42)
invalid_reaction_str = MockReaction(message=MockMessage(id=1))
invalid_reaction_str.__str__.return_value = ':longbeard:'
@@ -370,7 +399,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):
)
for reaction, user, expected, testname in cases:
with self.subTest(msg=f'Test with {testname} and expected return {expected}'):
- actual = snekbox.predicate_eval_emoji_reaction(valid_ctx, reaction, user)
+ actual = snekbox.predicate_emoji_reaction(valid_ctx, reaction, user)
self.assertEqual(actual, expected)
diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py
index 988b3857b..1bb678db2 100644
--- a/tests/bot/test_converters.py
+++ b/tests/bot/test_converters.py
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
from dateutil.relativedelta import relativedelta
from discord.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_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: