diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/base.py | 2 | ||||
| -rw-r--r-- | tests/bot/exts/backend/test_error_handler.py | 34 | ||||
| -rw-r--r-- | tests/bot/exts/filters/test_filtering.py | 40 | ||||
| -rw-r--r-- | tests/bot/exts/filters/test_token_remover.py | 4 | ||||
| -rw-r--r-- | tests/bot/exts/info/test_information.py | 15 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/infraction/test_utils.py | 20 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/test_incidents.py | 98 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/test_silence.py | 64 | ||||
| -rw-r--r-- | tests/bot/test_converters.py | 50 | ||||
| -rw-r--r-- | tests/bot/utils/test_checks.py | 1 | ||||
| -rw-r--r-- | tests/bot/utils/test_time.py | 27 | ||||
| -rw-r--r-- | tests/helpers.py | 27 | 
12 files changed, 274 insertions, 108 deletions
| diff --git a/tests/base.py b/tests/base.py index ab9287e9a..5e304ea9d 100644 --- a/tests/base.py +++ b/tests/base.py @@ -103,4 +103,4 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase):          with self.assertRaises(commands.MissingPermissions) as cm:              await cmd.can_run(ctx) -        self.assertCountEqual(permissions.keys(), cm.exception.missing_perms) +        self.assertCountEqual(permissions.keys(), cm.exception.missing_permissions) diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index 382194a63..35fa0ee59 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -107,7 +107,7 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase):          """Should send error with `ctx.send` when error is `CommandOnCooldown`."""          self.ctx.reset_mock()          cog = ErrorHandler(self.bot) -        error = errors.CommandOnCooldown(10, 9) +        error = errors.CommandOnCooldown(10, 9, type=None)          self.assertIsNone(await cog.on_command_error(self.ctx, error))          self.ctx.send.assert_awaited_once_with(error) @@ -544,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/filters/test_filtering.py b/tests/bot/exts/filters/test_filtering.py new file mode 100644 index 000000000..8ae59c1f1 --- /dev/null +++ b/tests/bot/exts/filters/test_filtering.py @@ -0,0 +1,40 @@ +import unittest +from unittest.mock import patch + +from bot.exts.filters import filtering +from tests.helpers import MockBot, autospec + + +class FilteringCogTests(unittest.IsolatedAsyncioTestCase): +    """Tests the `Filtering` cog.""" + +    def setUp(self): +        """Instantiate the bot and cog.""" +        self.bot = MockBot() +        with patch("bot.utils.scheduling.create_task", new=lambda task, **_: task.close()): +            self.cog = filtering.Filtering(self.bot) + +    @autospec(filtering.Filtering, "_get_filterlist_items", pass_mocks=False, return_value=["TOKEN"]) +    async def test_token_filter(self): +        """Ensure that a filter token is correctly detected in a message.""" +        messages = { +            "": False, +            "no matches": False, +            "TOKEN": True, + +            # See advisory https://github.com/python-discord/bot/security/advisories/GHSA-j8c3-8x46-8pp6 +            "https://google.com TOKEN": True, +            "https://google.com something else": False, +        } + +        for message, match in messages.items(): +            with self.subTest(input=message, match=match): +                result, _ = await self.cog._has_watch_regex_match(message) + +                self.assertEqual( +                    match, +                    bool(result), +                    msg=f"Hit was {'expected' if match else 'not expected'} for this input." +                ) +                if result: +                    self.assertEqual("TOKEN", result.group()) diff --git a/tests/bot/exts/filters/test_token_remover.py b/tests/bot/exts/filters/test_token_remover.py index 05e790723..4db27269a 100644 --- a/tests/bot/exts/filters/test_token_remover.py +++ b/tests/bot/exts/filters/test_token_remover.py @@ -26,7 +26,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):          self.msg.guild.get_member.return_value.bot = False          self.msg.guild.get_member.return_value.__str__.return_value = "Woody"          self.msg.author.__str__ = MagicMock(return_value=self.msg.author.name) -        self.msg.author.avatar_url_as.return_value = "picture-lemon.png" +        self.msg.author.display_avatar.url = "picture-lemon.png"      def test_extract_user_id_valid(self):          """Should consider user IDs valid if they decode into an integer ID.""" @@ -376,7 +376,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase):              colour=Colour(constants.Colours.soft_red),              title="Token removed!",              text=log_msg + "\n" + userid_log_message, -            thumbnail=self.msg.author.avatar_url_as.return_value, +            thumbnail=self.msg.author.display_avatar.url,              channel_id=constants.Channels.mod_alerts,              ping_everyone=True,          ) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index d8250befb..632287322 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -42,7 +42,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase):          embed = kwargs.pop('embed')          self.assertEqual(embed.title, "Role information (Total 1 role)") -        self.assertEqual(embed.colour, discord.Colour.blurple()) +        self.assertEqual(embed.colour, discord.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,7 +50,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase):          dummy_role = helpers.MockRole(              name="Dummy",              id=112233445566778899, -            colour=discord.Colour.blurple(), +            colour=discord.Colour.og_blurple(),              position=10,              members=[self.ctx.author],              permissions=discord.Permissions(0) @@ -80,7 +80,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase):          admin_embed = admin_kwargs["embed"]          self.assertEqual(dummy_embed.title, "Dummy info") -        self.assertEqual(dummy_embed.colour, discord.Colour.blurple()) +        self.assertEqual(dummy_embed.colour, discord.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}") @@ -417,14 +417,14 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          f"{COG_PATH}.basic_user_infraction_counts",          new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions"))      ) -    async def test_create_user_embed_uses_blurple_colour_when_user_has_no_roles(self): -        """The embed should be created with a blurple colour if the user has no assigned roles.""" +    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) -        self.assertEqual(embed.colour, discord.Colour.blurple()) +        self.assertEqual(embed.colour, discord.Colour.og_blurple())      @unittest.mock.patch(          f"{COG_PATH}.basic_user_infraction_counts", @@ -435,10 +435,9 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          ctx = helpers.MockContext()          user = helpers.MockMember(id=217, colour=0) -        user.avatar_url_as.return_value = "avatar url" +        user.display_avatar.url = "avatar url"          embed = await self.cog.create_user_embed(ctx, user) -        user.avatar_url_as.assert_called_once_with(static_format="png")          self.assertEqual(embed.thumbnail.url, "avatar url") diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index eb256f1fd..72eebb254 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -139,14 +139,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):                          type="Ban",                          expires="2020-02-26 09:20 (23 hours and 59 minutes)",                          reason="No reason provided." -                    ), +                    ) + utils.INFRACTION_APPEAL_SERVER_FOOTER,                      colour=Colours.soft_red,                      url=utils.RULES_URL                  ).set_author(                      name=utils.INFRACTION_AUTHOR_NAME,                      url=utils.RULES_URL,                      icon_url=Icons.token_removed -                ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER), +                ),                  "send_result": True              },              { @@ -157,14 +157,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):                          type="Warning",                          expires="N/A",                          reason="Test reason." -                    ), +                    ) + utils.INFRACTION_APPEAL_MODMAIL_FOOTER,                      colour=Colours.soft_red,                      url=utils.RULES_URL                  ).set_author(                      name=utils.INFRACTION_AUTHOR_NAME,                      url=utils.RULES_URL,                      icon_url=Icons.token_removed -                ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER), +                ),                  "send_result": False              },              # Note that this test case asserts that the DM that *would* get sent to the user is formatted @@ -177,14 +177,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):                          type="Note",                          expires="N/A",                          reason="No reason provided." -                    ), +                    ) + utils.INFRACTION_APPEAL_MODMAIL_FOOTER,                      colour=Colours.soft_red,                      url=utils.RULES_URL                  ).set_author(                      name=utils.INFRACTION_AUTHOR_NAME,                      url=utils.RULES_URL,                      icon_url=Icons.defcon_denied -                ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER), +                ),                  "send_result": False              },              { @@ -195,14 +195,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):                          type="Mute",                          expires="2020-02-26 09:20 (23 hours and 59 minutes)",                          reason="Test" -                    ), +                    ) + utils.INFRACTION_APPEAL_MODMAIL_FOOTER,                      colour=Colours.soft_red,                      url=utils.RULES_URL                  ).set_author(                      name=utils.INFRACTION_AUTHOR_NAME,                      url=utils.RULES_URL,                      icon_url=Icons.defcon_denied -                ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER), +                ),                  "send_result": False              },              { @@ -213,14 +213,14 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):                          type="Mute",                          expires="N/A",                          reason="foo bar" * 4000 -                    )[:4093] + "...", +                    )[:4093-utils.LONGEST_EXTRAS] + "..." + utils.INFRACTION_APPEAL_MODMAIL_FOOTER,                      colour=Colours.soft_red,                      url=utils.RULES_URL                  ).set_author(                      name=utils.INFRACTION_AUTHOR_NAME,                      url=utils.RULES_URL,                      icon_url=Icons.defcon_denied -                ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER), +                ),                  "send_result": True              }          ] diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index c98edf08a..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.mock import AsyncMock, MagicMock, call, patch +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. @@ -372,7 +391,7 @@ class TestArchive(TestIncidents):          # Define our own `incident` to be archived          incident = MockMessage(              content="this is an incident", -            author=MockUser(name="author_name", avatar_url="author_avatar"), +            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 @@ -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/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index ef8394be8..92ce3418a 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -431,7 +431,13 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):          asyncio.run(self.cog._async_init())  # Populate instance attributes.          self.text_channel = MockTextChannel() -        self.text_overwrite = PermissionOverwrite(send_messages=True, add_reactions=False) +        self.text_overwrite = PermissionOverwrite( +            send_messages=True, +            add_reactions=False, +            create_private_threads=True, +            create_public_threads=False, +            send_messages_in_threads=True +        )          self.text_channel.overwrites_for.return_value = self.text_overwrite          self.voice_channel = MockVoiceChannel() @@ -502,9 +508,39 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):      async def test_skipped_already_silenced(self):          """Permissions were not set and `False` was returned for an already silenced channel."""          subtests = ( -            (False, MockTextChannel(), PermissionOverwrite(send_messages=False, add_reactions=False)), -            (True, MockTextChannel(), PermissionOverwrite(send_messages=True, add_reactions=True)), -            (True, MockTextChannel(), PermissionOverwrite(send_messages=False, add_reactions=False)), +            ( +                False, +                MockTextChannel(), +                PermissionOverwrite( +                    send_messages=False, +                    add_reactions=False, +                    create_private_threads=False, +                    create_public_threads=False, +                    send_messages_in_threads=False +                ) +            ), +            ( +                True, +                MockTextChannel(), +                PermissionOverwrite( +                    send_messages=True, +                    add_reactions=True, +                    create_private_threads=True, +                    create_public_threads=True, +                    send_messages_in_threads=True +                ) +            ), +            ( +                True, +                MockTextChannel(), +                PermissionOverwrite( +                    send_messages=False, +                    add_reactions=False, +                    create_private_threads=False, +                    create_public_threads=False, +                    send_messages_in_threads=False +                ) +            ),              (False, MockVoiceChannel(), PermissionOverwrite(connect=False, speak=False)),              (True, MockVoiceChannel(), PermissionOverwrite(connect=True, speak=True)),              (True, MockVoiceChannel(), PermissionOverwrite(connect=False, speak=False)), @@ -552,11 +588,16 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):          await self.cog._set_silence_overwrites(self.text_channel)          new_overwrite_dict = dict(self.text_overwrite) -        # Remove 'send_messages' & 'add_reactions' keys because they were changed by the method. -        del prev_overwrite_dict['send_messages'] -        del prev_overwrite_dict['add_reactions'] -        del new_overwrite_dict['send_messages'] -        del new_overwrite_dict['add_reactions'] +        # Remove related permission keys because they were changed by the method. +        for perm_name in ( +                "send_messages", +                "add_reactions", +                "create_private_threads", +                "create_public_threads", +                "send_messages_in_threads" +        ): +            del prev_overwrite_dict[perm_name] +            del new_overwrite_dict[perm_name]          self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) @@ -594,7 +635,10 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):      async def test_cached_previous_overwrites(self):          """Channel's previous overwrites were cached.""" -        overwrite_json = '{"send_messages": true, "add_reactions": false}' +        overwrite_json = ( +            '{"send_messages": true, "add_reactions": false, "create_private_threads": true, ' +            '"create_public_threads": false, "send_messages_in_threads": true}' +        )          await self.cog._set_silence_overwrites(self.text_channel)          self.cog.previous_overwrites.set.assert_awaited_once_with(self.text_channel.id, overwrite_json) diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py index c23d66663..1bb678db2 100644 --- a/tests/bot/test_converters.py +++ b/tests/bot/test_converters.py @@ -1,6 +1,6 @@ -import datetime  import re  import unittest +from datetime import MAXYEAR, datetime, timezone  from unittest.mock import MagicMock, patch  from dateutil.relativedelta import relativedelta @@ -17,7 +17,7 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):          cls.context = MagicMock          cls.context.author = 'bob' -        cls.fixed_utc_now = datetime.datetime.fromisoformat('2019-01-01T00:00:00') +        cls.fixed_utc_now = datetime.fromisoformat('2019-01-01T00:00:00+00:00')      async def test_package_name_for_valid(self):          """PackageName returns valid package names unchanged.""" @@ -96,7 +96,7 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):              expected_datetime = self.fixed_utc_now + relativedelta(**duration_dict)              with patch('bot.converters.datetime') as mock_datetime: -                mock_datetime.utcnow.return_value = self.fixed_utc_now +                mock_datetime.now.return_value = self.fixed_utc_now                  with self.subTest(duration=duration, duration_dict=duration_dict):                      converted_datetime = await converter.convert(self.context, duration) @@ -142,52 +142,53 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):      async def test_duration_converter_out_of_range(self, mock_datetime):          """Duration converter should raise BadArgument if datetime raises a ValueError."""          mock_datetime.__add__.side_effect = ValueError -        mock_datetime.utcnow.return_value = mock_datetime +        mock_datetime.now.return_value = mock_datetime -        duration = f"{datetime.MAXYEAR}y" +        duration = f"{MAXYEAR}y"          exception_message = f"`{duration}` results in a datetime outside the supported range."          with self.assertRaisesRegex(BadArgument, re.escape(exception_message)):              await Duration().convert(self.context, duration)      async def test_isodatetime_converter_for_valid(self):          """ISODateTime converter returns correct datetime for valid datetime string.""" +        utc = timezone.utc          test_values = (              # `YYYY-mm-ddTHH:MM:SSZ` | `YYYY-mm-dd HH:MM:SSZ` -            ('2019-09-02T02:03:05Z', datetime.datetime(2019, 9, 2, 2, 3, 5)), -            ('2019-09-02 02:03:05Z', datetime.datetime(2019, 9, 2, 2, 3, 5)), +            ('2019-09-02T02:03:05Z', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)), +            ('2019-09-02 02:03:05Z', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),              # `YYYY-mm-ddTHH:MM:SS±HH:MM` | `YYYY-mm-dd HH:MM:SS±HH:MM` -            ('2019-09-02T03:18:05+01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)), -            ('2019-09-02 03:18:05+01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)), -            ('2019-09-02T00:48:05-01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)), -            ('2019-09-02 00:48:05-01:15', datetime.datetime(2019, 9, 2, 2, 3, 5)), +            ('2019-09-02T03:18:05+01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)), +            ('2019-09-02 03:18:05+01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)), +            ('2019-09-02T00:48:05-01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)), +            ('2019-09-02 00:48:05-01:15', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),              # `YYYY-mm-ddTHH:MM:SS±HHMM` | `YYYY-mm-dd HH:MM:SS±HHMM` -            ('2019-09-02T03:18:05+0115', datetime.datetime(2019, 9, 2, 2, 3, 5)), -            ('2019-09-02 03:18:05+0115', datetime.datetime(2019, 9, 2, 2, 3, 5)), -            ('2019-09-02T00:48:05-0115', datetime.datetime(2019, 9, 2, 2, 3, 5)), -            ('2019-09-02 00:48:05-0115', datetime.datetime(2019, 9, 2, 2, 3, 5)), +            ('2019-09-02T03:18:05+0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)), +            ('2019-09-02 03:18:05+0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)), +            ('2019-09-02T00:48:05-0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)), +            ('2019-09-02 00:48:05-0115', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),              # `YYYY-mm-ddTHH:MM:SS±HH` | `YYYY-mm-dd HH:MM:SS±HH` -            ('2019-09-02 03:03:05+01', datetime.datetime(2019, 9, 2, 2, 3, 5)), -            ('2019-09-02T01:03:05-01', datetime.datetime(2019, 9, 2, 2, 3, 5)), +            ('2019-09-02 03:03:05+01', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)), +            ('2019-09-02T01:03:05-01', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),              # `YYYY-mm-ddTHH:MM:SS` | `YYYY-mm-dd HH:MM:SS` -            ('2019-09-02T02:03:05', datetime.datetime(2019, 9, 2, 2, 3, 5)), -            ('2019-09-02 02:03:05', datetime.datetime(2019, 9, 2, 2, 3, 5)), +            ('2019-09-02T02:03:05', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)), +            ('2019-09-02 02:03:05', datetime(2019, 9, 2, 2, 3, 5, tzinfo=utc)),              # `YYYY-mm-ddTHH:MM` | `YYYY-mm-dd HH:MM` -            ('2019-11-12T09:15', datetime.datetime(2019, 11, 12, 9, 15)), -            ('2019-11-12 09:15', datetime.datetime(2019, 11, 12, 9, 15)), +            ('2019-11-12T09:15', datetime(2019, 11, 12, 9, 15, tzinfo=utc)), +            ('2019-11-12 09:15', datetime(2019, 11, 12, 9, 15, tzinfo=utc)),              # `YYYY-mm-dd` -            ('2019-04-01', datetime.datetime(2019, 4, 1)), +            ('2019-04-01', datetime(2019, 4, 1, tzinfo=utc)),              # `YYYY-mm` -            ('2019-02-01', datetime.datetime(2019, 2, 1)), +            ('2019-02-01', datetime(2019, 2, 1, tzinfo=utc)),              # `YYYY` -            ('2025', datetime.datetime(2025, 1, 1)), +            ('2025', datetime(2025, 1, 1, tzinfo=utc)),          )          converter = ISODateTime() @@ -195,7 +196,6 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):          for datetime_string, expected_dt in test_values:              with self.subTest(datetime_string=datetime_string, expected_dt=expected_dt):                  converted_dt = await converter.convert(self.context, datetime_string) -                self.assertIsNone(converted_dt.tzinfo)                  self.assertEqual(converted_dt, expected_dt)      async def test_isodatetime_converter_for_invalid(self): diff --git a/tests/bot/utils/test_checks.py b/tests/bot/utils/test_checks.py index 883465e0b..4ae11d5d3 100644 --- a/tests/bot/utils/test_checks.py +++ b/tests/bot/utils/test_checks.py @@ -32,6 +32,7 @@ class ChecksTests(unittest.IsolatedAsyncioTestCase):      async def test_has_no_roles_check_without_guild(self):          """`has_no_roles_check` should return `False` when `Context.guild` is None."""          self.ctx.channel = MagicMock(DMChannel) +        self.ctx.guild = None          self.assertFalse(await checks.has_no_roles_check(self.ctx))      async def test_has_no_roles_check_returns_false_with_unwanted_role(self): diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py index 8edffd1c9..a3dcbfc0a 100644 --- a/tests/bot/utils/test_time.py +++ b/tests/bot/utils/test_time.py @@ -72,9 +72,9 @@ class TimeTests(unittest.TestCase):      def test_format_infraction_with_duration_custom_units(self):          """format_infraction_with_duration should work for custom max_units."""          test_cases = ( -            ('3000-12-12T00:01:00Z', datetime(3000, 12, 11, 12, 5, 5), 6, +            ('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)'), -            ('3000-11-23T20:09:00Z', datetime(3000, 4, 25, 20, 15), 20, +            ('3000-11-23T20:09:00Z', datetime(3000, 4, 25, 20, 15, tzinfo=timezone.utc), 20,               '<t:32531918940:f> (6 months, 28 days, 23 hours and 54 minutes)')          ) @@ -84,16 +84,21 @@ class TimeTests(unittest.TestCase):      def test_format_infraction_with_duration_normal_usage(self):          """format_infraction_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), 2, '<t:1576108860:f> (12 hours and 55 seconds)'), -            ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5), 1, '<t:1576108860:f> (12 hours)'), -            ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59), 2, '<t:1576108800:f> (1 minute)'), -            ('2019-11-23T20:09:00Z', datetime(2019, 11, 15, 20, 15), 2, '<t:1574539740:f> (7 days and 23 hours)'), -            ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15), 2, '<t:1574539740:f> (6 months and 28 days)'), -            ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 20, 53), 2, '<t:1574542680:f> (5 minutes)'), -            ('2019-11-24T00:00:00Z', datetime(2019, 11, 23, 23, 59, 0), 2, '<t:1574553600:f> (1 minute)'), -            ('2019-11-23T23:59:00Z', datetime(2017, 7, 21, 23, 0), 2, '<t:1574553540:f> (2 years and 4 months)'), -            ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5), 2, +            ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5, tzinfo=utc), 2, +                '<t:1576108860:f> (12 hours and 55 seconds)'), +            ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5, tzinfo=utc), 1, '<t:1576108860:f> (12 hours)'), +            ('2019-12-12T00:00:00Z', datetime(2019, 12, 11, 23, 59, tzinfo=utc), 2, '<t:1576108800:f> (1 minute)'), +            ('2019-11-23T20:09:00Z', datetime(2019, 11, 15, 20, 15, tzinfo=utc), 2, +                '<t:1574539740:f> (7 days and 23 hours)'), +            ('2019-11-23T20:09:00Z', datetime(2019, 4, 25, 20, 15, tzinfo=utc), 2, +                '<t:1574539740:f> (6 months and 28 days)'), +            ('2019-11-23T20:58:00Z', datetime(2019, 11, 23, 20, 53, tzinfo=utc), 2, '<t:1574542680:f> (5 minutes)'), +            ('2019-11-24T00:00:00Z', datetime(2019, 11, 23, 23, 59, 0, tzinfo=utc), 2, '<t:1574553600:f> (1 minute)'), +            ('2019-11-23T23:59:00Z', datetime(2017, 7, 21, 23, 0, tzinfo=utc), 2, +             '<t:1574553540:f> (2 years and 4 months)'), +            ('2019-11-23T23:59:00Z', datetime(2019, 11, 23, 23, 49, 5, tzinfo=utc), 2,               '<t:1574553540:f> (9 minutes and 55 seconds)'),              (None, datetime(2019, 11, 23, 23, 49, 5), 2, None),          ) diff --git a/tests/helpers.py b/tests/helpers.py index 83b9b2363..9d4988d23 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -39,7 +39,7 @@ class HashableMixin(discord.mixins.EqualityComparable):  class ColourMixin: -    """A mixin for Mocks that provides the aliasing of color->colour like discord.py does.""" +    """A mixin for Mocks that provides the aliasing of (accent_)color->(accent_)colour like discord.py does."""      @property      def color(self) -> discord.Colour: @@ -49,6 +49,14 @@ class ColourMixin:      def color(self, color: discord.Colour) -> None:          self.colour = color +    @property +    def accent_color(self) -> discord.Colour: +        return self.accent_colour + +    @accent_color.setter +    def accent_color(self, color: discord.Colour) -> None: +        self.accent_colour = color +  class CustomMockMixin:      """ @@ -242,7 +250,13 @@ class MockMember(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin  # Create a User instance to get a realistic Mock of `discord.User` -user_instance = discord.User(data=unittest.mock.MagicMock(), state=unittest.mock.MagicMock()) +_user_data_mock = collections.defaultdict(unittest.mock.MagicMock, { +    "accent_color": 0 +}) +user_instance = discord.User( +    data=unittest.mock.MagicMock(get=unittest.mock.Mock(side_effect=_user_data_mock.get)), +    state=unittest.mock.MagicMock() +)  class MockUser(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): @@ -428,7 +442,12 @@ message_instance = discord.Message(state=state, channel=channel, data=message_da  # Create a Context instance to get a realistic MagicMock of `discord.ext.commands.Context` -context_instance = Context(message=unittest.mock.MagicMock(), prefix=unittest.mock.MagicMock()) +context_instance = Context( +    message=unittest.mock.MagicMock(), +    prefix="$", +    bot=MockBot(), +    view=None +)  context_instance.invoked_from_error_handler = None @@ -537,7 +556,7 @@ class MockReaction(CustomMockMixin, unittest.mock.MagicMock):          self.__str__.return_value = str(self.emoji) -webhook_instance = discord.Webhook(data=unittest.mock.MagicMock(), adapter=unittest.mock.MagicMock()) +webhook_instance = discord.Webhook(data=unittest.mock.MagicMock(), session=unittest.mock.MagicMock())  class MockAsyncWebhook(CustomMockMixin, unittest.mock.MagicMock): | 
