diff options
| author | 2021-05-05 11:47:38 +0100 | |
|---|---|---|
| committer | 2021-05-05 11:47:38 +0100 | |
| commit | a5d9cc8a138e9065036dc6da28e2829b541649f7 (patch) | |
| tree | 8788b5bd1c541a98ce2af22ff52bb619d0e1f400 /tests | |
| parent | Resolved issues (diff) | |
| parent | Merge pull request #1517 from python-discord/is-vs-== (diff) | |
Merge branch 'main' into master
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/README.md | 2 | ||||
| -rw-r--r-- | tests/bot/exts/backend/test_error_handler.py | 550 | ||||
| -rw-r--r-- | tests/bot/exts/info/doc/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/info/doc/test_parsing.py | 66 | ||||
| -rw-r--r-- | tests/bot/exts/info/test_information.py | 22 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/infraction/test_infractions.py | 2 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/infraction/test_utils.py | 12 | ||||
| -rw-r--r-- | tests/bot/test_converters.py | 21 | ||||
| -rw-r--r-- | tests/bot/utils/test_services.py | 4 | ||||
| -rw-r--r-- | tests/helpers.py | 2 | 
10 files changed, 650 insertions, 31 deletions
| diff --git a/tests/README.md b/tests/README.md index 4f62edd68..092324123 100644 --- a/tests/README.md +++ b/tests/README.md @@ -114,7 +114,7 @@ class BotCogTests(unittest.TestCase):  ### Mocking coroutines -By default, the `unittest.mock.Mock` and `unittest.mock.MagicMock` classes cannot mock coroutines, since the `__call__` method they provide is synchronous. In anticipation of the `AsyncMock` that will be [introduced in Python 3.8](https://docs.python.org/3.9/whatsnew/3.8.html#unittest), we have added an `AsyncMock` helper to [`helpers.py`](/tests/helpers.py). Do note that this drop-in replacement only implements an asynchronous `__call__` method, not the additional assertions that will come with the new `AsyncMock` type in Python 3.8. +By default, the `unittest.mock.Mock` and `unittest.mock.MagicMock` classes cannot mock coroutines, since the `__call__` method they provide is synchronous. The [`AsyncMock`](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.AsyncMock) that has been [introduced in Python 3.8](https://docs.python.org/3.9/whatsnew/3.8.html#unittest) is an asynchronous version of `MagicMock` that can be used anywhere a coroutine is expected.  ### Special mocks for some `discord.py` types diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py new file mode 100644 index 000000000..bd4fb5942 --- /dev/null +++ b/tests/bot/exts/backend/test_error_handler.py @@ -0,0 +1,550 @@ +import unittest +from unittest.mock import AsyncMock, MagicMock, call, patch + +from discord.ext.commands import errors + +from bot.api import ResponseCodeError +from bot.errors import InvalidInfractedUser, LockedResourceError +from bot.exts.backend.error_handler import ErrorHandler, setup +from bot.exts.info.tags import Tags +from bot.exts.moderation.silence import Silence +from bot.utils.checks import InWhitelistCheckFailure +from tests.helpers import MockBot, MockContext, MockGuild, MockRole + + +class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): +    """Tests for error handler functionality.""" + +    def setUp(self): +        self.bot = MockBot() +        self.ctx = MockContext(bot=self.bot) + +    async def test_error_handler_already_handled(self): +        """Should not do anything when error is already handled by local error handler.""" +        self.ctx.reset_mock() +        cog = ErrorHandler(self.bot) +        error = errors.CommandError() +        error.handled = "foo" +        self.assertIsNone(await cog.on_command_error(self.ctx, error)) +        self.ctx.send.assert_not_awaited() + +    async def test_error_handler_command_not_found_error_not_invoked_by_handler(self): +        """Should try first (un)silence channel, when fail, try to get tag.""" +        error = errors.CommandNotFound() +        test_cases = ( +            { +                "try_silence_return": True, +                "called_try_get_tag": False +            }, +            { +                "try_silence_return": False, +                "called_try_get_tag": False +            }, +            { +                "try_silence_return": False, +                "called_try_get_tag": True +            } +        ) +        cog = ErrorHandler(self.bot) +        cog.try_silence = AsyncMock() +        cog.try_get_tag = AsyncMock() + +        for case in test_cases: +            with self.subTest(try_silence_return=case["try_silence_return"], try_get_tag=case["called_try_get_tag"]): +                self.ctx.reset_mock() +                cog.try_silence.reset_mock(return_value=True) +                cog.try_get_tag.reset_mock() + +                cog.try_silence.return_value = case["try_silence_return"] +                self.ctx.channel.id = 1234 + +                self.assertIsNone(await cog.on_command_error(self.ctx, error)) + +                if case["try_silence_return"]: +                    cog.try_get_tag.assert_not_awaited() +                    cog.try_silence.assert_awaited_once() +                else: +                    cog.try_silence.assert_awaited_once() +                    cog.try_get_tag.assert_awaited_once() + +                self.ctx.send.assert_not_awaited() + +    async def test_error_handler_command_not_found_error_invoked_by_handler(self): +        """Should do nothing when error is `CommandNotFound` and have attribute `invoked_from_error_handler`.""" +        ctx = MockContext(bot=self.bot, invoked_from_error_handler=True) + +        cog = ErrorHandler(self.bot) +        cog.try_silence = AsyncMock() +        cog.try_get_tag = AsyncMock() + +        error = errors.CommandNotFound() + +        self.assertIsNone(await cog.on_command_error(ctx, error)) + +        cog.try_silence.assert_not_awaited() +        cog.try_get_tag.assert_not_awaited() +        self.ctx.send.assert_not_awaited() + +    async def test_error_handler_user_input_error(self): +        """Should await `ErrorHandler.handle_user_input_error` when error is `UserInputError`.""" +        self.ctx.reset_mock() +        cog = ErrorHandler(self.bot) +        cog.handle_user_input_error = AsyncMock() +        error = errors.UserInputError() +        self.assertIsNone(await cog.on_command_error(self.ctx, error)) +        cog.handle_user_input_error.assert_awaited_once_with(self.ctx, error) + +    async def test_error_handler_check_failure(self): +        """Should await `ErrorHandler.handle_check_failure` when error is `CheckFailure`.""" +        self.ctx.reset_mock() +        cog = ErrorHandler(self.bot) +        cog.handle_check_failure = AsyncMock() +        error = errors.CheckFailure() +        self.assertIsNone(await cog.on_command_error(self.ctx, error)) +        cog.handle_check_failure.assert_awaited_once_with(self.ctx, error) + +    async def test_error_handler_command_on_cooldown(self): +        """Should send error with `ctx.send` when error is `CommandOnCooldown`.""" +        self.ctx.reset_mock() +        cog = ErrorHandler(self.bot) +        error = errors.CommandOnCooldown(10, 9) +        self.assertIsNone(await cog.on_command_error(self.ctx, error)) +        self.ctx.send.assert_awaited_once_with(error) + +    async def test_error_handler_command_invoke_error(self): +        """Should call `handle_api_error` or `handle_unexpected_error` depending on original error.""" +        cog = ErrorHandler(self.bot) +        cog.handle_api_error = AsyncMock() +        cog.handle_unexpected_error = AsyncMock() +        test_cases = ( +            { +                "args": (self.ctx, errors.CommandInvokeError(ResponseCodeError(AsyncMock()))), +                "expect_mock_call": cog.handle_api_error +            }, +            { +                "args": (self.ctx, errors.CommandInvokeError(TypeError)), +                "expect_mock_call": cog.handle_unexpected_error +            }, +            { +                "args": (self.ctx, errors.CommandInvokeError(LockedResourceError("abc", "test"))), +                "expect_mock_call": "send" +            }, +            { +                "args": (self.ctx, errors.CommandInvokeError(InvalidInfractedUser(self.ctx.author))), +                "expect_mock_call": "send" +            } +        ) + +        for case in test_cases: +            with self.subTest(args=case["args"], expect_mock_call=case["expect_mock_call"]): +                self.ctx.send.reset_mock() +                self.assertIsNone(await cog.on_command_error(*case["args"])) +                if case["expect_mock_call"] == "send": +                    self.ctx.send.assert_awaited_once() +                else: +                    case["expect_mock_call"].assert_awaited_once_with( +                        self.ctx, case["args"][1].original +                    ) + +    async def test_error_handler_conversion_error(self): +        """Should call `handle_api_error` or `handle_unexpected_error` depending on original error.""" +        cog = ErrorHandler(self.bot) +        cog.handle_api_error = AsyncMock() +        cog.handle_unexpected_error = AsyncMock() +        cases = ( +            { +                "error": errors.ConversionError(AsyncMock(), ResponseCodeError(AsyncMock())), +                "mock_function_to_call": cog.handle_api_error +            }, +            { +                "error": errors.ConversionError(AsyncMock(), TypeError), +                "mock_function_to_call": cog.handle_unexpected_error +            } +        ) + +        for case in cases: +            with self.subTest(**case): +                self.assertIsNone(await cog.on_command_error(self.ctx, case["error"])) +                case["mock_function_to_call"].assert_awaited_once_with(self.ctx, case["error"].original) + +    async def test_error_handler_two_other_errors(self): +        """Should call `handle_unexpected_error` if error is `MaxConcurrencyReached` or `ExtensionError`.""" +        cog = ErrorHandler(self.bot) +        cog.handle_unexpected_error = AsyncMock() +        errs = ( +            errors.MaxConcurrencyReached(1, MagicMock()), +            errors.ExtensionError(name="foo") +        ) + +        for err in errs: +            with self.subTest(error=err): +                cog.handle_unexpected_error.reset_mock() +                self.assertIsNone(await cog.on_command_error(self.ctx, err)) +                cog.handle_unexpected_error.assert_awaited_once_with(self.ctx, err) + +    @patch("bot.exts.backend.error_handler.log") +    async def test_error_handler_other_errors(self, log_mock): +        """Should `log.debug` other errors.""" +        cog = ErrorHandler(self.bot) +        error = errors.DisabledCommand()  # Use this just as a other error +        self.assertIsNone(await cog.on_command_error(self.ctx, error)) +        log_mock.debug.assert_called_once() + + +class TrySilenceTests(unittest.IsolatedAsyncioTestCase): +    """Test for helper functions that handle `CommandNotFound` error.""" + +    def setUp(self): +        self.bot = MockBot() +        self.silence = Silence(self.bot) +        self.bot.get_command.return_value = self.silence.silence +        self.ctx = MockContext(bot=self.bot) +        self.cog = ErrorHandler(self.bot) + +    async def test_try_silence_context_invoked_from_error_handler(self): +        """Should set `Context.invoked_from_error_handler` to `True`.""" +        self.ctx.invoked_with = "foo" +        await self.cog.try_silence(self.ctx) +        self.assertTrue(hasattr(self.ctx, "invoked_from_error_handler")) +        self.assertTrue(self.ctx.invoked_from_error_handler) + +    async def test_try_silence_get_command(self): +        """Should call `get_command` with `silence`.""" +        self.ctx.invoked_with = "foo" +        await self.cog.try_silence(self.ctx) +        self.bot.get_command.assert_called_once_with("silence") + +    async def test_try_silence_no_permissions_to_run(self): +        """Should return `False` because missing permissions.""" +        self.ctx.invoked_with = "foo" +        self.bot.get_command.return_value.can_run = AsyncMock(return_value=False) +        self.assertFalse(await self.cog.try_silence(self.ctx)) + +    async def test_try_silence_no_permissions_to_run_command_error(self): +        """Should return `False` because `CommandError` raised (no permissions).""" +        self.ctx.invoked_with = "foo" +        self.bot.get_command.return_value.can_run = AsyncMock(side_effect=errors.CommandError()) +        self.assertFalse(await self.cog.try_silence(self.ctx)) + +    async def test_try_silence_silencing(self): +        """Should run silence command with correct arguments.""" +        self.bot.get_command.return_value.can_run = AsyncMock(return_value=True) +        test_cases = ("shh", "shhh", "shhhhhh", "shhhhhhhhhhhhhhhhhhh") + +        for case in test_cases: +            with self.subTest(message=case): +                self.ctx.reset_mock() +                self.ctx.invoked_with = case +                self.assertTrue(await self.cog.try_silence(self.ctx)) +                self.ctx.invoke.assert_awaited_once_with( +                    self.bot.get_command.return_value, +                    duration=min(case.count("h")*2, 15) +                ) + +    async def test_try_silence_unsilence(self): +        """Should call unsilence command.""" +        self.silence.silence.can_run = AsyncMock(return_value=True) +        test_cases = ("unshh", "unshhhhh", "unshhhhhhhhh") + +        for case in test_cases: +            with self.subTest(message=case): +                self.bot.get_command.side_effect = (self.silence.silence, self.silence.unsilence) +                self.ctx.reset_mock() +                self.ctx.invoked_with = case +                self.assertTrue(await self.cog.try_silence(self.ctx)) +                self.ctx.invoke.assert_awaited_once_with(self.silence.unsilence) + +    async def test_try_silence_no_match(self): +        """Should return `False` when message don't match.""" +        self.ctx.invoked_with = "foo" +        self.assertFalse(await self.cog.try_silence(self.ctx)) + + +class TryGetTagTests(unittest.IsolatedAsyncioTestCase): +    """Tests for `try_get_tag` function.""" + +    def setUp(self): +        self.bot = MockBot() +        self.ctx = MockContext() +        self.tag = Tags(self.bot) +        self.cog = ErrorHandler(self.bot) +        self.bot.get_command.return_value = self.tag.get_command + +    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) + +    async def test_try_get_tag_no_permissions(self): +        """Test how to handle checks failing.""" +        self.tag.get_command.can_run = AsyncMock(return_value=False) +        self.ctx.invoked_with = "foo" +        self.assertIsNone(await self.cog.try_get_tag(self.ctx)) + +    async def test_try_get_tag_command_error(self): +        """Should call `on_command_error` when `CommandError` raised.""" +        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.invoke = AsyncMock(return_value=True) +        self.cog.send_command_suggestion = AsyncMock() + +        await self.cog.try_get_tag(self.ctx) +        self.cog.send_command_suggestion.assert_not_awaited() + +    @patch("bot.exts.backend.error_handler.MODERATION_ROLES", new=[1234]) +    async def test_dont_call_suggestion_if_user_mod(self): +        """Should not call command suggestion if user is a mod.""" +        self.ctx.invoked_with = "foo" +        self.ctx.invoke = AsyncMock(return_value=False) +        self.ctx.author.roles = [MockRole(id=1234)] +        self.cog.send_command_suggestion = AsyncMock() + +        await self.cog.try_get_tag(self.ctx) +        self.cog.send_command_suggestion.assert_not_awaited() + +    async def test_call_suggestion(self): +        """Should call command suggestion if user is not a mod.""" +        self.ctx.invoked_with = "foo" +        self.ctx.invoke = AsyncMock(return_value=False) +        self.cog.send_command_suggestion = AsyncMock() + +        await self.cog.try_get_tag(self.ctx) +        self.cog.send_command_suggestion.assert_awaited_once_with(self.ctx, "foo") + + +class IndividualErrorHandlerTests(unittest.IsolatedAsyncioTestCase): +    """Individual error categories handler tests.""" + +    def setUp(self): +        self.bot = MockBot() +        self.ctx = MockContext(bot=self.bot) +        self.cog = ErrorHandler(self.bot) + +    async def test_handle_input_error_handler_errors(self): +        """Should handle each error probably.""" +        test_cases = ( +            { +                "error": errors.MissingRequiredArgument(MagicMock()), +                "call_prepared": True +            }, +            { +                "error": errors.TooManyArguments(), +                "call_prepared": True +            }, +            { +                "error": errors.BadArgument(), +                "call_prepared": True +            }, +            { +                "error": errors.BadUnionArgument(MagicMock(), MagicMock(), MagicMock()), +                "call_prepared": True +            }, +            { +                "error": errors.ArgumentParsingError(), +                "call_prepared": False +            }, +            { +                "error": errors.UserInputError(), +                "call_prepared": True +            } +        ) + +        for case in test_cases: +            with self.subTest(error=case["error"], call_prepared=case["call_prepared"]): +                self.ctx.reset_mock() +                self.assertIsNone(await self.cog.handle_user_input_error(self.ctx, case["error"])) +                self.ctx.send.assert_awaited_once() +                if case["call_prepared"]: +                    self.ctx.send_help.assert_awaited_once() +                else: +                    self.ctx.send_help.assert_not_awaited() + +    async def test_handle_check_failure_errors(self): +        """Should await `ctx.send` when error is check failure.""" +        test_cases = ( +            { +                "error": errors.BotMissingPermissions(MagicMock()), +                "call_ctx_send": True +            }, +            { +                "error": errors.BotMissingRole(MagicMock()), +                "call_ctx_send": True +            }, +            { +                "error": errors.BotMissingAnyRole(MagicMock()), +                "call_ctx_send": True +            }, +            { +                "error": errors.NoPrivateMessage(), +                "call_ctx_send": True +            }, +            { +                "error": InWhitelistCheckFailure(1234), +                "call_ctx_send": True +            }, +            { +                "error": ResponseCodeError(MagicMock()), +                "call_ctx_send": False +            } +        ) + +        for case in test_cases: +            with self.subTest(error=case["error"], call_ctx_send=case["call_ctx_send"]): +                self.ctx.reset_mock() +                await self.cog.handle_check_failure(self.ctx, case["error"]) +                if case["call_ctx_send"]: +                    self.ctx.send.assert_awaited_once() +                else: +                    self.ctx.send.assert_not_awaited() + +    @patch("bot.exts.backend.error_handler.log") +    async def test_handle_api_error(self, log_mock): +        """Should `ctx.send` on HTTP error codes, `log.debug|warning` depends on code.""" +        test_cases = ( +            { +                "error": ResponseCodeError(AsyncMock(status=400)), +                "log_level": "debug" +            }, +            { +                "error": ResponseCodeError(AsyncMock(status=404)), +                "log_level": "debug" +            }, +            { +                "error": ResponseCodeError(AsyncMock(status=550)), +                "log_level": "warning" +            }, +            { +                "error": ResponseCodeError(AsyncMock(status=1000)), +                "log_level": "warning" +            } +        ) + +        for case in test_cases: +            with self.subTest(error=case["error"], log_level=case["log_level"]): +                self.ctx.reset_mock() +                log_mock.reset_mock() +                await self.cog.handle_api_error(self.ctx, case["error"]) +                self.ctx.send.assert_awaited_once() +                if case["log_level"] == "warning": +                    log_mock.warning.assert_called_once() +                else: +                    log_mock.debug.assert_called_once() + +    @patch("bot.exts.backend.error_handler.push_scope") +    @patch("bot.exts.backend.error_handler.log") +    async def test_handle_unexpected_error(self, log_mock, push_scope_mock): +        """Should `ctx.send` this error, error log this and sent to Sentry.""" +        for case in (None, MockGuild()): +            with self.subTest(guild=case): +                self.ctx.reset_mock() +                log_mock.reset_mock() +                push_scope_mock.reset_mock() + +                self.ctx.guild = case +                await self.cog.handle_unexpected_error(self.ctx, errors.CommandError()) + +                self.ctx.send.assert_awaited_once() +                log_mock.error.assert_called_once() +                push_scope_mock.assert_called_once() + +                set_tag_calls = [ +                    call("command", self.ctx.command.qualified_name), +                    call("message_id", self.ctx.message.id), +                    call("channel_id", self.ctx.channel.id), +                ] +                set_extra_calls = [ +                    call("full_message", self.ctx.message.content) +                ] +                if case: +                    url = ( +                        f"https://discordapp.com/channels/" +                        f"{self.ctx.guild.id}/{self.ctx.channel.id}/{self.ctx.message.id}" +                    ) +                    set_extra_calls.append(call("jump_to", url)) + +                push_scope_mock.set_tag.has_calls(set_tag_calls) +                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.""" + +    def test_setup(self): +        """Should call `bot.add_cog` with `ErrorHandler`.""" +        bot = MockBot() +        setup(bot) +        bot.add_cog.assert_called_once() diff --git a/tests/bot/exts/info/doc/__init__.py b/tests/bot/exts/info/doc/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/info/doc/__init__.py diff --git a/tests/bot/exts/info/doc/test_parsing.py b/tests/bot/exts/info/doc/test_parsing.py new file mode 100644 index 000000000..1663d8491 --- /dev/null +++ b/tests/bot/exts/info/doc/test_parsing.py @@ -0,0 +1,66 @@ +from unittest import TestCase + +from bot.exts.info.doc import _parsing as parsing + + +class SignatureSplitter(TestCase): + +    def test_basic_split(self): +        test_cases = ( +            ("0,0,0", ["0", "0", "0"]), +            ("0,a=0,a=0", ["0", "a=0", "a=0"]), +        ) +        self._run_tests(test_cases) + +    def test_commas_ignored_in_brackets(self): +        test_cases = ( +            ("0,[0,0],0,[0,0],0", ["0", "[0,0]", "0", "[0,0]", "0"]), +            ("(0,),0,(0,(0,),0),0", ["(0,)", "0", "(0,(0,),0)", "0"]), +        ) +        self._run_tests(test_cases) + +    def test_mixed_brackets(self): +        tests_cases = ( +            ("[0,{0},0],0,{0:0},0", ["[0,{0},0]", "0", "{0:0}", "0"]), +            ("([0],0,0),0,(0,0),0", ["([0],0,0)", "0", "(0,0)", "0"]), +            ("([(0,),(0,)],0),0", ["([(0,),(0,)],0)", "0"]), +        ) +        self._run_tests(tests_cases) + +    def test_string_contents_ignored(self): +        test_cases = ( +            ("'0,0',0,',',0", ["'0,0'", "0", "','", "0"]), +            ("0,[']',0],0", ["0", "[']',0]", "0"]), +            ("{0,0,'}}',0,'{'},0", ["{0,0,'}}',0,'{'}", "0"]), +        ) +        self._run_tests(test_cases) + +    def test_mixed_quotes(self): +        test_cases = ( +            ("\"0',0',\",'0,0',0", ["\"0',0',\"", "'0,0'", "0"]), +            ("\",',\",'\",',0", ["\",',\"", "'\",'", "0"]), +        ) +        self._run_tests(test_cases) + +    def test_quote_escaped(self): +        test_cases = ( +            (r"'\',','\\',0", [r"'\','", r"'\\'", "0"]), +            (r"'0\',0\\\'\\',0", [r"'0\',0\\\'\\'", "0"]), +        ) +        self._run_tests(test_cases) + +    def test_real_signatures(self): +        test_cases = ( +            ("start, stop[, step]", ["start", " stop[, step]"]), +            ("object=b'', encoding='utf-8', errors='strict'", ["object=b''", " encoding='utf-8'", " errors='strict'"]), +            ( +                "typename, field_names, *, rename=False, defaults=None, module=None", +                ["typename", " field_names", " *", " rename=False", " defaults=None", " module=None"] +            ), +        ) +        self._run_tests(test_cases) + +    def _run_tests(self, test_cases): +        for input_string, expected_output in test_cases: +            with self.subTest(input_string=input_string): +                self.assertEqual(list(parsing._split_parameters(input_string)), expected_output) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 80731c9f0..770660fe3 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -281,8 +281,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          """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))          user = helpers.MockMember() +        user.public_flags = unittest.mock.MagicMock(verified_bot=False)          user.nick = None          user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock") +        user.colour = 0          embed = await self.cog.create_user_embed(ctx, user) @@ -296,8 +298,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          """The embed should use the nick if it's available."""          ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1))          user = helpers.MockMember() +        user.public_flags = unittest.mock.MagicMock(verified_bot=False)          user.nick = "Cat lover"          user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock") +        user.colour = 0          embed = await self.cog.create_user_embed(ctx, user) @@ -311,10 +315,9 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          """Created `!user` embeds should not contain mention of the @everyone-role."""          ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1))          admins_role = helpers.MockRole(name='Admins') -        admins_role.colour = 100          # A `MockMember` has the @Everyone role by default; we add the Admins to that. -        user = helpers.MockMember(roles=[admins_role], top_role=admins_role) +        user = helpers.MockMember(roles=[admins_role], colour=100)          embed = await self.cog.create_user_embed(ctx, user) @@ -332,12 +335,11 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=50))          moderators_role = helpers.MockRole(name='Moderators') -        moderators_role.colour = 100          infraction_counts.return_value = ("Infractions", "expanded infractions info")          nomination_counts.return_value = ("Nominations", "nomination info") -        user = helpers.MockMember(id=314, roles=[moderators_role], top_role=moderators_role) +        user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)          embed = await self.cog.create_user_embed(ctx, user)          infraction_counts.assert_called_once_with(user) @@ -367,11 +369,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=100))          moderators_role = helpers.MockRole(name='Moderators') -        moderators_role.colour = 100          infraction_counts.return_value = ("Infractions", "basic infractions info") -        user = helpers.MockMember(id=314, roles=[moderators_role], top_role=moderators_role) +        user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)          embed = await self.cog.create_user_embed(ctx, user)          infraction_counts.assert_called_once_with(user) @@ -407,12 +408,11 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          ctx = helpers.MockContext()          moderators_role = helpers.MockRole(name='Moderators') -        moderators_role.colour = 100 -        user = helpers.MockMember(id=314, roles=[moderators_role], top_role=moderators_role) +        user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)          embed = await self.cog.create_user_embed(ctx, user) -        self.assertEqual(embed.colour, discord.Colour(moderators_role.colour)) +        self.assertEqual(embed.colour, discord.Colour(100))      @unittest.mock.patch(          f"{COG_PATH}.basic_user_infraction_counts", @@ -422,7 +422,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          """The embed should be created with a blurple colour if the user has no assigned roles."""          ctx = helpers.MockContext() -        user = helpers.MockMember(id=217) +        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()) @@ -435,7 +435,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):          """The embed thumbnail should be set to the user's avatar in `png` format."""          ctx = helpers.MockContext() -        user = helpers.MockMember(id=217) +        user = helpers.MockMember(id=217, colour=0)          user.avatar_url_as.return_value = "avatar url"          embed = await self.cog.create_user_embed(ctx, user) diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 86c2617ea..08f39cd50 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -39,7 +39,7 @@ class TruncationTests(unittest.IsolatedAsyncioTestCase):              delete_message_days=0          )          self.cog.apply_infraction.assert_awaited_once_with( -            self.ctx, {"foo": "bar"}, self.target, self.ctx.guild.ban.return_value +            self.ctx, {"foo": "bar", "purge": ""}, self.target, self.ctx.guild.ban.return_value          )      @patch("bot.exts.moderation.infraction._utils.post_infraction") diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 5b62463e0..ee9ff650c 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -146,7 +146,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):                      name=utils.INFRACTION_AUTHOR_NAME,                      url=utils.RULES_URL,                      icon_url=Icons.token_removed -                ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER), +                ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER),                  "send_result": True              },              { @@ -164,9 +164,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):                      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 +            # correctly, even though that message is deliberately never sent.              {                  "args": (self.user, "note", None, None, Icons.defcon_denied),                  "expected_output": Embed( @@ -182,7 +184,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):                      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              },              { @@ -200,7 +202,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):                      name=utils.INFRACTION_AUTHOR_NAME,                      url=utils.RULES_URL,                      icon_url=Icons.defcon_denied -                ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER), +                ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER),                  "send_result": False              },              { @@ -218,7 +220,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):                      name=utils.INFRACTION_AUTHOR_NAME,                      url=utils.RULES_URL,                      icon_url=Icons.defcon_denied -                ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER), +                ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER),                  "send_result": True              }          ] diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py index c42111f3f..4af84dde5 100644 --- a/tests/bot/test_converters.py +++ b/tests/bot/test_converters.py @@ -10,9 +10,9 @@ from bot.converters import (      Duration,      HushDurationConverter,      ISODateTime, +    PackageName,      TagContentConverter,      TagNameConverter, -    ValidPythonIdentifier,  ) @@ -78,24 +78,23 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase):                  with self.assertRaisesRegex(BadArgument, re.escape(exception_message)):                      await TagNameConverter.convert(self.context, invalid_name) -    async def test_valid_python_identifier_for_valid(self): -        """ValidPythonIdentifier returns valid identifiers unchanged.""" -        test_values = ('foo', 'lemon') +    async def test_package_name_for_valid(self): +        """PackageName returns valid package names unchanged.""" +        test_values = ('foo', 'le_mon', 'num83r')          for name in test_values:              with self.subTest(identifier=name): -                conversion = await ValidPythonIdentifier.convert(self.context, name) +                conversion = await PackageName.convert(self.context, name)                  self.assertEqual(name, conversion) -    async def test_valid_python_identifier_for_invalid(self): -        """ValidPythonIdentifier raises the proper exception for invalid identifiers.""" -        test_values = ('nested.stuff', '#####') +    async def test_package_name_for_invalid(self): +        """PackageName raises the proper exception for invalid package names.""" +        test_values = ('text_with_a_dot.', 'UpperCaseName', 'dashed-name')          for name in test_values:              with self.subTest(identifier=name): -                exception_message = f'`{name}` is not a valid Python identifier' -                with self.assertRaisesRegex(BadArgument, re.escape(exception_message)): -                    await ValidPythonIdentifier.convert(self.context, name) +                with self.assertRaises(BadArgument): +                    await PackageName.convert(self.context, name)      async def test_duration_converter_for_valid(self):          """Duration returns the correct `datetime` for valid duration strings.""" diff --git a/tests/bot/utils/test_services.py b/tests/bot/utils/test_services.py index 1b48f6560..3b71022db 100644 --- a/tests/bot/utils/test_services.py +++ b/tests/bot/utils/test_services.py @@ -30,9 +30,9 @@ class PasteTests(unittest.IsolatedAsyncioTestCase):          """Url with specified extension is returned on successful requests."""          key = "paste_key"          test_cases = ( -            (f"https://paste_service.com/{key}.txt", "txt"), +            (f"https://paste_service.com/{key}.txt?noredirect", "txt"),              (f"https://paste_service.com/{key}.py", "py"), -            (f"https://paste_service.com/{key}", ""), +            (f"https://paste_service.com/{key}?noredirect", ""),          )          response = MagicMock(              json=AsyncMock(return_value={"key": key}) diff --git a/tests/helpers.py b/tests/helpers.py index 496363ae3..e3dc5fe5b 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -385,6 +385,7 @@ 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.invoked_from_error_handler = None  class MockContext(CustomMockMixin, unittest.mock.MagicMock): @@ -402,6 +403,7 @@ class MockContext(CustomMockMixin, unittest.mock.MagicMock):          self.guild = kwargs.get('guild', MockGuild())          self.author = kwargs.get('author', MockMember())          self.channel = kwargs.get('channel', MockTextChannel()) +        self.invoked_from_error_handler = kwargs.get('invoked_from_error_handler', False)  attachment_instance = discord.Attachment(data=unittest.mock.MagicMock(id=1), state=unittest.mock.MagicMock()) | 
