aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/README.md12
-rw-r--r--tests/bot/exts/backend/test_error_handler.py550
-rw-r--r--tests/bot/exts/info/doc/__init__.py0
-rw-r--r--tests/bot/exts/info/doc/test_parsing.py66
-rw-r--r--tests/bot/exts/info/test_information.py22
-rw-r--r--tests/bot/exts/moderation/infraction/test_infractions.py4
-rw-r--r--tests/bot/test_converters.py21
-rw-r--r--tests/bot/utils/test_services.py4
-rw-r--r--tests/bot/utils/test_time.py13
-rw-r--r--tests/helpers.py2
10 files changed, 649 insertions, 45 deletions
diff --git a/tests/README.md b/tests/README.md
index 4f62edd68..1a17c09bd 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -12,13 +12,13 @@ We are using the following modules and packages for our unit tests:
- [unittest.mock](https://docs.python.org/3/library/unittest.mock.html) (standard library)
- [coverage.py](https://coverage.readthedocs.io/en/stable/)
-To ensure the results you obtain on your personal machine are comparable to those generated in the Azure pipeline, please make sure to run your tests with the virtual environment defined by our [Pipfile](/Pipfile). To run your tests with `pipenv`, we've provided two "scripts" shortcuts:
+To ensure the results you obtain on your personal machine are comparable to those generated in the CI, please make sure to run your tests with the virtual environment defined by our [Poetry Project](/pyproject.toml). To run your tests with `poetry`, we've provided two "scripts" shortcuts:
-- `pipenv run test` will run `unittest` with `coverage.py`
-- `pipenv run test path/to/test.py` will run a specific test.
-- `pipenv run report` will generate a coverage report of the tests you've run with `pipenv run test`. If you append the `-m` flag to this command, the report will include the lines and branches not covered by tests in addition to the test coverage report.
+- `poetry run task test` will run `unittest` with `coverage.py`
+- `poetry run task test path/to/test.py` will run a specific test.
+- `poetry run task report` will generate a coverage report of the tests you've run with `poetry run task test`. If you append the `-m` flag to this command, the report will include the lines and branches not covered by tests in addition to the test coverage report.
-If you want a coverage report, make sure to run the tests with `pipenv run test` *first*.
+If you want a coverage report, make sure to run the tests with `poetry run task test` *first*.
## Writing tests
@@ -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 08f39cd50..b9d527770 100644
--- a/tests/bot/exts/moderation/infraction/test_infractions.py
+++ b/tests/bot/exts/moderation/infraction/test_infractions.py
@@ -74,7 +74,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):
"""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")
+ self.cog.apply_voice_ban.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."""
@@ -184,7 +184,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase):
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)
+ post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_ban", None, active=True, expires_at=None)
apply_infraction_mock.assert_called_once_with(self.cog, self.ctx, infraction, user, ANY)
# Test action
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/bot/utils/test_time.py b/tests/bot/utils/test_time.py
index 694d3a40f..115ddfb0d 100644
--- a/tests/bot/utils/test_time.py
+++ b/tests/bot/utils/test_time.py
@@ -1,7 +1,5 @@
-import asyncio
import unittest
from datetime import datetime, timezone
-from unittest.mock import AsyncMock, patch
from dateutil.relativedelta import relativedelta
@@ -56,17 +54,6 @@ class TimeTests(unittest.TestCase):
"""Testing format_infraction."""
self.assertEqual(time.format_infraction('2019-12-12T00:01:00Z'), '2019-12-12 00:01')
- @patch('asyncio.sleep', new_callable=AsyncMock)
- def test_wait_until(self, mock):
- """Testing wait_until."""
- start = datetime(2019, 1, 1, 0, 0)
- then = datetime(2019, 1, 1, 0, 10)
-
- # No return value
- self.assertIs(asyncio.run(time.wait_until(then, start)), None)
-
- mock.assert_called_once_with(10 * 60)
-
def test_format_infraction_with_duration_none_expiry(self):
"""format_infraction_with_duration should work for None expiry."""
test_cases = (
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())