diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/bot/exts/info/doc/test_parsing.py | 23 | ||||
| -rw-r--r-- | tests/bot/exts/utils/snekbox/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/utils/snekbox/test_io.py | 34 | ||||
| -rw-r--r-- | tests/bot/exts/utils/snekbox/test_snekbox.py (renamed from tests/bot/exts/utils/test_snekbox.py) | 266 | 
4 files changed, 242 insertions, 81 deletions
| diff --git a/tests/bot/exts/info/doc/test_parsing.py b/tests/bot/exts/info/doc/test_parsing.py index 1663d8491..d2105a53c 100644 --- a/tests/bot/exts/info/doc/test_parsing.py +++ b/tests/bot/exts/info/doc/test_parsing.py @@ -1,6 +1,7 @@  from unittest import TestCase  from bot.exts.info.doc import _parsing as parsing +from bot.exts.info.doc._markdown import DocMarkdownConverter  class SignatureSplitter(TestCase): @@ -64,3 +65,25 @@ class SignatureSplitter(TestCase):          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) + + +class MarkdownConverterTest(TestCase): +    def test_hr_removed(self): +        test_cases = ( +            ('<hr class="docutils"/>', ""), +            ("<hr>", ""), +        ) +        self._run_tests(test_cases) + +    def test_whitespace_removed(self): +        test_cases = ( +            ("lines\nof\ntext", "lines of text"), +            ("lines\n\nof\n\ntext", "lines of text"), +        ) +        self._run_tests(test_cases) + +    def _run_tests(self, test_cases: tuple[tuple[str, str], ...]): +        for input_string, expected_output in test_cases: +            with self.subTest(input_string=input_string): +                d = DocMarkdownConverter(page_url="https://example.com") +                self.assertEqual(d.convert(input_string), expected_output) diff --git a/tests/bot/exts/utils/snekbox/__init__.py b/tests/bot/exts/utils/snekbox/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/utils/snekbox/__init__.py diff --git a/tests/bot/exts/utils/snekbox/test_io.py b/tests/bot/exts/utils/snekbox/test_io.py new file mode 100644 index 000000000..bcf1162b8 --- /dev/null +++ b/tests/bot/exts/utils/snekbox/test_io.py @@ -0,0 +1,34 @@ +from unittest import TestCase + +# noinspection PyProtectedMember +from bot.exts.utils.snekbox import _io + + +class SnekboxIOTests(TestCase): +    # noinspection SpellCheckingInspection +    def test_normalize_file_name(self): +        """Invalid file names should be normalized.""" +        cases = [ +            # ANSI escape sequences -> underscore +            (r"\u001b[31mText", "_Text"), +            # (Multiple consecutive should be collapsed to one underscore) +            (r"a\u001b[35m\u001b[37mb", "a_b"), +            # Backslash escaped chars -> underscore +            (r"\n", "_"), +            (r"\r", "_"), +            (r"A\0\tB", "A__B"), +            # Any other disallowed chars -> underscore +            (r"\\.txt", "_.txt"), +            (r"A!@#$%^&*B, C()[]{}+=D.txt", "A_B_C_D.txt"),  # noqa: P103 +            (" ", "_"), +            # Normal file names should be unchanged +            ("legal_file-name.txt", "legal_file-name.txt"), +            ("_-.", "_-."), +        ] +        for name, expected in cases: +            with self.subTest(name=name, expected=expected): +                # Test function directly +                self.assertEqual(_io.normalize_discord_file_name(name), expected) +                # Test FileAttachment.to_file() +                obj = _io.FileAttachment(name, b"") +                self.assertEqual(obj.to_file().filename, expected) diff --git a/tests/bot/exts/utils/test_snekbox.py b/tests/bot/exts/utils/snekbox/test_snekbox.py index b1f32c210..9dcf7fd8c 100644 --- a/tests/bot/exts/utils/test_snekbox.py +++ b/tests/bot/exts/utils/snekbox/test_snekbox.py @@ -1,5 +1,6 @@  import asyncio  import unittest +from base64 import b64encode  from unittest.mock import AsyncMock, MagicMock, Mock, call, create_autospec, patch  from discord import AllowedMentions @@ -8,7 +9,8 @@ from discord.ext import commands  from bot import constants  from bot.errors import LockedResourceError  from bot.exts.utils import snekbox -from bot.exts.utils.snekbox import Snekbox +from bot.exts.utils.snekbox import EvalJob, EvalResult, Snekbox +from bot.exts.utils.snekbox._io import FileAttachment  from tests.helpers import MockBot, MockContext, MockMember, MockMessage, MockReaction, MockUser @@ -17,34 +19,55 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          """Add mocked bot and cog to the instance."""          self.bot = MockBot()          self.cog = Snekbox(bot=self.bot) +        self.job = EvalJob.from_code("import random") + +    @staticmethod +    def code_args(code: str) -> tuple[EvalJob]: +        """Converts code to a tuple of arguments expected.""" +        return EvalJob.from_code(code),      async def test_post_job(self):          """Post the eval code to the URLs.snekbox_eval_api endpoint."""          resp = MagicMock() -        resp.json = AsyncMock(return_value="return") +        resp.json = AsyncMock(return_value={"stdout": "Hi", "returncode": 137, "files": []})          context_manager = MagicMock()          context_manager.__aenter__.return_value = resp          self.bot.http_session.post.return_value = context_manager -        self.assertEqual(await self.cog.post_job("import random", "3.10"), "return") +        job = EvalJob.from_code("import random").as_version("3.10") +        self.assertEqual(await self.cog.post_job(job), EvalResult("Hi", 137)) + +        expected = { +            "args": ["main.py"], +            "files": [ +                { +                    "path": "main.py", +                    "content": b64encode("import random".encode()).decode() +                } +            ] +        }          self.bot.http_session.post.assert_called_with(              constants.URLs.snekbox_eval_api, -            json={"input": "import random"}, +            json=expected,              raise_for_status=True          )          resp.json.assert_awaited_once()      async def test_upload_output_reject_too_long(self):          """Reject output longer than MAX_PASTE_LENGTH.""" -        result = await self.cog.upload_output("-" * (snekbox.MAX_PASTE_LENGTH + 1)) +        result = await self.cog.upload_output("-" * (snekbox._cog.MAX_PASTE_LENGTH + 1))          self.assertEqual(result, "too long to upload") -    @patch("bot.exts.utils.snekbox.send_to_paste_service") +    @patch("bot.exts.utils.snekbox._cog.send_to_paste_service")      async def test_upload_output(self, mock_paste_util):          """Upload the eval output to the URLs.paste_service.format(key="documents") endpoint."""          await self.cog.upload_output("Test output.") -        mock_paste_util.assert_called_once_with("Test output.", extension="txt", max_length=snekbox.MAX_PASTE_LENGTH) +        mock_paste_util.assert_called_once_with( +            "Test output.", +            extension="txt", +            max_length=snekbox._cog.MAX_PASTE_LENGTH +        )      async def test_codeblock_converter(self):          ctx = MockContext() @@ -76,40 +99,94 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):              (['x = 1', 'print(x)', 'print("Some other code.")'], 'x = 1', 'three blocks of code')          ) -        for case, setup_code, testname in cases: -            setup = snekbox.TIMEIT_SETUP_WRAPPER.format(setup=setup_code) -            expected = ('\n'.join(case[1:] if setup_code else case), [*base_args, setup]) -            with self.subTest(msg=f'Test with {testname} and expected return {expected}'): +        for case, setup_code, test_name in cases: +            setup = snekbox._cog.TIMEIT_SETUP_WRAPPER.format(setup=setup_code) +            expected = [*base_args, setup, '\n'.join(case[1:] if setup_code else case)] +            with self.subTest(msg=f'Test with {test_name} and expected return {expected}'):                  self.assertEqual(self.cog.prepare_timeit_input(case), expected) -    def test_get_results_message(self): -        """Return error and message according to the eval result.""" +    def test_eval_result_message(self): +        """EvalResult.get_message(), should return message."""          cases = ( -            ('ERROR', None, ('Your 3.11 eval job has failed', 'ERROR')), -            ('', 128 + snekbox.SIGKILL, ('Your 3.11 eval job timed out or ran out of memory', '')), -            ('', 255, ('Your 3.11 eval job has failed', 'A fatal NsJail error occurred')) +            ('ERROR', None, ('Your 3.11 eval job has failed', 'ERROR', '')), +            ('', 128 + snekbox._eval.SIGKILL, ('Your 3.11 eval job timed out or ran out of memory', '', '')), +            ('', 255, ('Your 3.11 eval job has failed', 'A fatal NsJail error occurred', ''))          )          for stdout, returncode, expected in cases: +            exp_msg, exp_err, exp_files_err = expected              with self.subTest(stdout=stdout, returncode=returncode, expected=expected): -                actual = self.cog.get_results_message({'stdout': stdout, 'returncode': returncode}, 'eval', '3.11') -                self.assertEqual(actual, expected) - -    @patch('bot.exts.utils.snekbox.Signals', side_effect=ValueError) -    def test_get_results_message_invalid_signal(self, mock_signals: Mock): +                result = EvalResult(stdout=stdout, returncode=returncode) +                job = EvalJob([]) +                # Check all 3 message types +                msg = result.get_message(job) +                self.assertEqual(msg, exp_msg) +                error = result.error_message +                self.assertEqual(error, exp_err) +                files_error = result.files_error_message +                self.assertEqual(files_error, exp_files_err) + +    @patch("bot.exts.utils.snekbox._eval.FILE_COUNT_LIMIT", 2) +    def test_eval_result_files_error_message(self): +        """EvalResult.files_error_message, should return files error message.""" +        cases = [ +            ([], ["abc"], ( +                "1 file upload (abc) failed because its file size exceeds 8 MiB." +            )), +            ([], ["file1.bin", "f2.bin"], ( +                "2 file uploads (file1.bin, f2.bin) failed because each file's size exceeds 8 MiB." +            )), +            (["a", "b"], ["c"], ( +                "1 file upload (c) failed as it exceeded the 2 file limit." +            )), +            (["a"], ["b", "c"], ( +                "2 file uploads (b, c) failed as they exceeded the 2 file limit." +            )), +        ] +        for files, failed_files, expected_msg in cases: +            with self.subTest(files=files, failed_files=failed_files, expected_msg=expected_msg): +                result = EvalResult("", 0, files, failed_files) +                msg = result.files_error_message +                self.assertIn(expected_msg, msg) + +    @patch("bot.exts.utils.snekbox._eval.FILE_COUNT_LIMIT", 2) +    def test_eval_result_files_error_str(self): +        """EvalResult.files_error_message, should return files error message.""" +        cases = [ +            # Normal +            (["x.ini"], "x.ini"), +            (["123456", "879"], "123456, 879"), +            # Break on whole name if less than 3 characters remaining +            (["12345678", "9"], "12345678, ..."), +            # Otherwise break on max chars +            (["123", "345", "67890000"], "123, 345, 6789..."), +            (["abcdefg1234567"], "abcdefg123..."), +        ] +        for failed_files, expected in cases: +            with self.subTest(failed_files=failed_files, expected=expected): +                result = EvalResult("", 0, [], failed_files) +                msg = result.get_failed_files_str(char_max=10) +                self.assertEqual(msg, expected) + +    @patch('bot.exts.utils.snekbox._eval.Signals', side_effect=ValueError) +    def test_eval_result_message_invalid_signal(self, _mock_signals: Mock): +        result = EvalResult(stdout="", returncode=127)          self.assertEqual( -            self.cog.get_results_message({'stdout': '', 'returncode': 127}, 'eval', '3.11'), -            ('Your 3.11 eval job has completed with return code 127', '') +            result.get_message(EvalJob([], version="3.10")), +            "Your 3.10 eval job has completed with return code 127"          ) +        self.assertEqual(result.error_message, "") +        self.assertEqual(result.files_error_message, "") -    @patch('bot.exts.utils.snekbox.Signals') -    def test_get_results_message_valid_signal(self, mock_signals: Mock): -        mock_signals.return_value.name = 'SIGTEST' +    @patch('bot.exts.utils.snekbox._eval.Signals') +    def test_eval_result_message_valid_signal(self, mock_signals: Mock): +        mock_signals.return_value.name = "SIGTEST" +        result = EvalResult(stdout="", returncode=127)          self.assertEqual( -            self.cog.get_results_message({'stdout': '', 'returncode': 127}, 'eval', '3.11'), -            ('Your 3.11 eval job has completed with return code 127 (SIGTEST)', '') +            result.get_message(EvalJob([], version="3.11")), +            "Your 3.11 eval job has completed with return code 127 (SIGTEST)"          ) -    def test_get_status_emoji(self): +    def test_eval_result_status_emoji(self):          """Return emoji according to the eval result."""          cases = (              (' ', -1, ':warning:'), @@ -118,8 +195,8 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          )          for stdout, returncode, expected in cases:              with self.subTest(stdout=stdout, returncode=returncode, expected=expected): -                actual = self.cog.get_status_emoji({'stdout': stdout, 'returncode': returncode}) -                self.assertEqual(actual, expected) +                result = EvalResult(stdout=stdout, returncode=returncode) +                self.assertEqual(result.status_emoji, expected)      async def test_format_output(self):          """Test output formatting.""" @@ -178,10 +255,11 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          ctx.command = MagicMock()          self.cog.send_job = AsyncMock(return_value=response) -        self.cog.continue_job = AsyncMock(return_value=(None, None)) +        self.cog.continue_job = AsyncMock(return_value=None)          await self.cog.eval_command(self.cog, ctx=ctx, python_version='3.11', code=['MyAwesomeCode']) -        self.cog.send_job.assert_called_once_with(ctx, '3.11', 'MyAwesomeCode', args=None, job_name='eval') +        job = EvalJob.from_code("MyAwesomeCode") +        self.cog.send_job.assert_called_once_with(ctx, job)          self.cog.continue_job.assert_called_once_with(ctx, response, 'eval')      async def test_eval_command_evaluate_twice(self): @@ -191,13 +269,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          ctx.command = MagicMock()          self.cog.send_job = AsyncMock(return_value=response)          self.cog.continue_job = AsyncMock() -        self.cog.continue_job.side_effect = (('MyAwesomeFormattedCode', None), (None, None)) +        self.cog.continue_job.side_effect = (EvalJob.from_code('MyAwesomeFormattedCode'), None)          await self.cog.eval_command(self.cog, ctx=ctx, python_version='3.11', code=['MyAwesomeCode']) -        self.cog.send_job.assert_called_with( -            ctx, '3.11', 'MyAwesomeFormattedCode', args=None, job_name='eval' -        ) -        self.cog.continue_job.assert_called_with(ctx, response, 'eval') + +        expected_job = EvalJob.from_code("MyAwesomeFormattedCode") +        self.cog.send_job.assert_called_with(ctx, expected_job) +        self.cog.continue_job.assert_called_with(ctx, response, "eval")      async def test_eval_command_reject_two_eval_at_the_same_time(self):          """Test if the eval command rejects an eval if the author already have a running eval.""" @@ -212,8 +290,8 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          self.cog.post_job = AsyncMock(side_effect=delay_with_side_effect)          with self.assertRaises(LockedResourceError):              await asyncio.gather( -                self.cog.send_job(ctx, '3.11', 'MyAwesomeCode', job_name='eval'), -                self.cog.send_job(ctx, '3.11', 'MyAwesomeCode', job_name='eval'), +                self.cog.send_job(ctx, EvalJob.from_code("MyAwesomeCode")), +                self.cog.send_job(ctx, EvalJob.from_code("MyAwesomeCode")),              )      async def test_send_job(self): @@ -223,30 +301,31 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          ctx.send = AsyncMock()          ctx.author = MockUser(mention='@LemonLemonishBeard#0042') -        self.cog.post_job = AsyncMock(return_value={'stdout': '', 'returncode': 0}) -        self.cog.get_results_message = MagicMock(return_value=('Return code 0', '')) -        self.cog.get_status_emoji = MagicMock(return_value=':yay!:') +        eval_result = EvalResult("", 0) +        self.cog.post_job = AsyncMock(return_value=eval_result)          self.cog.format_output = AsyncMock(return_value=('[No output]', None)) +        self.cog.upload_output = AsyncMock()  # Should not be called          mocked_filter_cog = MagicMock()          mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=False)          self.bot.get_cog.return_value = mocked_filter_cog -        await self.cog.send_job(ctx, '3.11', 'MyAwesomeCode', job_name='eval') +        job = EvalJob.from_code('MyAwesomeCode') +        await self.cog.send_job(ctx, job),          ctx.send.assert_called_once()          self.assertEqual(              ctx.send.call_args.args[0], -            '@LemonLemonishBeard#0042 :yay!: Return code 0.\n\n```\n[No output]\n```' +            '@LemonLemonishBeard#0042 :warning: Your 3.11 eval job has completed ' +            'with return code 0.\n\n```\n[No output]\n```'          )          allowed_mentions = ctx.send.call_args.kwargs['allowed_mentions']          expected_allowed_mentions = AllowedMentions(everyone=False, roles=False, users=[ctx.author])          self.assertEqual(allowed_mentions.to_dict(), expected_allowed_mentions.to_dict()) -        self.cog.post_job.assert_called_once_with('MyAwesomeCode', '3.11', args=None) -        self.cog.get_status_emoji.assert_called_once_with({'stdout': '', 'returncode': 0}) -        self.cog.get_results_message.assert_called_once_with({'stdout': '', 'returncode': 0}, 'eval', '3.11') +        self.cog.post_job.assert_called_once_with(job)          self.cog.format_output.assert_called_once_with('') +        self.cog.upload_output.assert_not_called()      async def test_send_job_with_paste_link(self):          """Test the send_job function with a too long output that generate a paste link.""" @@ -255,29 +334,26 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          ctx.send = AsyncMock()          ctx.author.mention = '@LemonLemonishBeard#0042' -        self.cog.post_job = AsyncMock(return_value={'stdout': 'Way too long beard', 'returncode': 0}) -        self.cog.get_results_message = MagicMock(return_value=('Return code 0', '')) -        self.cog.get_status_emoji = MagicMock(return_value=':yay!:') +        eval_result = EvalResult("Way too long beard", 0) +        self.cog.post_job = AsyncMock(return_value=eval_result)          self.cog.format_output = AsyncMock(return_value=('Way too long beard', 'lookatmybeard.com'))          mocked_filter_cog = MagicMock()          mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=False)          self.bot.get_cog.return_value = mocked_filter_cog -        await self.cog.send_job(ctx, '3.11', 'MyAwesomeCode', job_name='eval') +        job = EvalJob.from_code("MyAwesomeCode").as_version("3.11") +        await self.cog.send_job(ctx, job),          ctx.send.assert_called_once()          self.assertEqual(              ctx.send.call_args.args[0], -            '@LemonLemonishBeard#0042 :yay!: Return code 0.' +            '@LemonLemonishBeard#0042 :white_check_mark: Your 3.11 eval job ' +            'has completed with return code 0.'              '\n\n```\nWay too long beard\n```\nFull output: lookatmybeard.com'          ) -        self.cog.post_job.assert_called_once_with('MyAwesomeCode', '3.11', args=None) -        self.cog.get_status_emoji.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0}) -        self.cog.get_results_message.assert_called_once_with( -            {'stdout': 'Way too long beard', 'returncode': 0}, 'eval', '3.11' -        ) +        self.cog.post_job.assert_called_once_with(job)          self.cog.format_output.assert_called_once_with('Way too long beard')      async def test_send_job_with_non_zero_eval(self): @@ -286,29 +362,57 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          ctx.message = MockMessage()          ctx.send = AsyncMock()          ctx.author.mention = '@LemonLemonishBeard#0042' -        self.cog.post_job = AsyncMock(return_value={'stdout': 'ERROR', 'returncode': 127}) -        self.cog.get_results_message = MagicMock(return_value=('Return code 127', 'Beard got stuck in the eval')) -        self.cog.get_status_emoji = MagicMock(return_value=':nope!:') -        self.cog.format_output = AsyncMock()  # This function isn't called + +        eval_result = EvalResult("ERROR", 127) +        self.cog.post_job = AsyncMock(return_value=eval_result) +        self.cog.upload_output = AsyncMock()  # This function isn't called          mocked_filter_cog = MagicMock()          mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=False)          self.bot.get_cog.return_value = mocked_filter_cog -        await self.cog.send_job(ctx, '3.11', 'MyAwesomeCode', job_name='eval') +        job = EvalJob.from_code("MyAwesomeCode").as_version("3.11") +        await self.cog.send_job(ctx, job),          ctx.send.assert_called_once()          self.assertEqual(              ctx.send.call_args.args[0], -            '@LemonLemonishBeard#0042 :nope!: Return code 127.\n\n```\nBeard got stuck in the eval\n```' +            '@LemonLemonishBeard#0042 :x: Your 3.11 eval job has completed with return code 127.' +            '\n\n```\nERROR\n```' +        ) + +        self.cog.post_job.assert_called_once_with(job) +        self.cog.upload_output.assert_not_called() + +    async def test_send_job_with_disallowed_file_ext(self): +        """Test send_job with disallowed file extensions.""" +        ctx = MockContext() +        ctx.message = MockMessage() +        ctx.send = AsyncMock() +        ctx.author.mention = "@user#7700" + +        eval_result = EvalResult("", 0, files=[FileAttachment("test.disallowed", b"test")]) +        self.cog.post_job = AsyncMock(return_value=eval_result) +        self.cog.upload_output = AsyncMock()  # This function isn't called + +        mocked_filter_cog = MagicMock() +        mocked_filter_cog.filter_snekbox_output = AsyncMock(return_value=False) +        self.bot.get_cog.return_value = mocked_filter_cog + +        job = EvalJob.from_code("MyAwesomeCode").as_version("3.11") +        await self.cog.send_job(ctx, job), + +        ctx.send.assert_called_once() +        res = ctx.send.call_args.args[0] +        self.assertTrue( +            res.startswith("@user#7700 :white_check_mark: Your 3.11 eval job has completed with return code 0.")          ) +        self.assertIn("Files with disallowed extensions can't be uploaded: **.disallowed**", res) -        self.cog.post_job.assert_called_once_with('MyAwesomeCode', '3.11', args=None) -        self.cog.get_status_emoji.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}) -        self.cog.get_results_message.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}, 'eval', '3.11') -        self.cog.format_output.assert_not_called() +        self.cog.post_job.assert_called_once_with(job) +        self.cog.upload_output.assert_not_called() -    @patch("bot.exts.utils.snekbox.partial") +    @patch("bot.exts.utils.snekbox._cog.partial")      async def test_continue_job_does_continue(self, partial_mock):          """Test that the continue_job function does continue if required conditions are met."""          ctx = MockContext( @@ -328,19 +432,19 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          actual = await self.cog.continue_job(ctx, response, self.cog.eval_command)          self.cog.get_code.assert_awaited_once_with(new_msg, ctx.command) -        self.assertEqual(actual, (expected, None)) +        self.assertEqual(actual, EvalJob.from_code(expected))          self.bot.wait_for.assert_has_awaits(              (                  call(                      'message_edit', -                    check=partial_mock(snekbox.predicate_message_edit, ctx), -                    timeout=snekbox.REDO_TIMEOUT, +                    check=partial_mock(snekbox._cog.predicate_message_edit, ctx), +                    timeout=snekbox._cog.REDO_TIMEOUT,                  ), -                call('reaction_add', check=partial_mock(snekbox.predicate_emoji_reaction, ctx), timeout=10) +                call('reaction_add', check=partial_mock(snekbox._cog.predicate_emoji_reaction, ctx), timeout=10)              )          ) -        ctx.message.add_reaction.assert_called_once_with(snekbox.REDO_EMOJI) -        ctx.message.clear_reaction.assert_called_once_with(snekbox.REDO_EMOJI) +        ctx.message.add_reaction.assert_called_once_with(snekbox._cog.REDO_EMOJI) +        ctx.message.clear_reaction.assert_called_once_with(snekbox._cog.REDO_EMOJI)          response.delete.assert_called_once()      async def test_continue_job_does_not_continue(self): @@ -348,8 +452,8 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          self.bot.wait_for.side_effect = asyncio.TimeoutError          actual = await self.cog.continue_job(ctx, MockMessage(), self.cog.eval_command) -        self.assertEqual(actual, (None, None)) -        ctx.message.clear_reaction.assert_called_once_with(snekbox.REDO_EMOJI) +        self.assertEqual(actual, None) +        ctx.message.clear_reaction.assert_called_once_with(snekbox._cog.REDO_EMOJI)      async def test_get_code(self):          """Should return 1st arg (or None) if eval cmd in message, otherwise return full content.""" @@ -391,18 +495,18 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          for ctx_msg, new_msg, expected, testname in cases:              with self.subTest(msg=f'Messages with {testname} return {expected}'):                  ctx = MockContext(message=ctx_msg) -                actual = snekbox.predicate_message_edit(ctx, ctx_msg, new_msg) +                actual = snekbox._cog.predicate_message_edit(ctx, ctx_msg, new_msg)                  self.assertEqual(actual, expected)      def test_predicate_emoji_reaction(self):          """Test the predicate_emoji_reaction function."""          valid_reaction = MockReaction(message=MockMessage(id=1)) -        valid_reaction.__str__.return_value = snekbox.REDO_EMOJI +        valid_reaction.__str__.return_value = snekbox._cog.REDO_EMOJI          valid_ctx = MockContext(message=MockMessage(id=1), author=MockUser(id=2))          valid_user = MockUser(id=2)          invalid_reaction_id = MockReaction(message=MockMessage(id=42)) -        invalid_reaction_id.__str__.return_value = snekbox.REDO_EMOJI +        invalid_reaction_id.__str__.return_value = snekbox._cog.REDO_EMOJI          invalid_user_id = MockUser(id=42)          invalid_reaction_str = MockReaction(message=MockMessage(id=1))          invalid_reaction_str.__str__.return_value = ':longbeard:' @@ -415,7 +519,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase):          )          for reaction, user, expected, testname in cases:              with self.subTest(msg=f'Test with {testname} and expected return {expected}'): -                actual = snekbox.predicate_emoji_reaction(valid_ctx, reaction, user) +                actual = snekbox._cog.predicate_emoji_reaction(valid_ctx, reaction, user)                  self.assertEqual(actual, expected) | 
