aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorGravatar wookie184 <[email protected]>2023-03-25 15:32:30 +0000
committerGravatar wookie184 <[email protected]>2023-03-25 15:32:30 +0000
commit1069fcdd601bf8ecc6518b0c02684a40ea299432 (patch)
tree87cdb7799d7128860e08ca8e586ff1c5bfd8626c /tests
parentMake show_* arguments to list_nominations keyword only (diff)
parentBump beautifulsoup4 from 4.11.2 to 4.12.0 (#2480) (diff)
Merge branch 'main' into 2302-activity-in-reviews
Diffstat (limited to 'tests')
-rw-r--r--tests/bot/exts/backend/test_error_handler.py14
-rw-r--r--tests/bot/exts/info/doc/test_parsing.py23
-rw-r--r--tests/bot/exts/recruitment/talentpool/test_review.py14
-rw-r--r--tests/bot/exts/utils/snekbox/__init__.py0
-rw-r--r--tests/bot/exts/utils/snekbox/test_io.py34
-rw-r--r--tests/bot/exts/utils/snekbox/test_snekbox.py (renamed from tests/bot/exts/utils/test_snekbox.py)266
-rw-r--r--tests/helpers.py31
7 files changed, 291 insertions, 91 deletions
diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py
index 092de0556..0ba2fcf11 100644
--- a/tests/bot/exts/backend/test_error_handler.py
+++ b/tests/bot/exts/backend/test_error_handler.py
@@ -334,13 +334,13 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase):
self.ctx = MockContext()
self.tag = Tags(self.bot)
self.cog = error_handler.ErrorHandler(self.bot)
- self.bot.get_command.return_value = self.tag.get_command
+ self.bot.get_cog.return_value = self.tag
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.bot.get_cog.reset_mock()
await self.cog.try_get_tag(self.ctx)
- self.bot.get_command.assert_called_once_with("tags get")
+ self.bot.get_cog.assert_called_once_with("Tags")
async def test_try_get_tag_invoked_from_error_handler(self):
"""`self.ctx` should have `invoked_from_error_handler` `True`."""
@@ -350,14 +350,14 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase):
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.bot.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.bot.can_run = AsyncMock(side_effect=err)
self.cog.on_command_error = AsyncMock()
self.assertIsNone(await self.cog.try_get_tag(self.ctx))
self.cog.on_command_error.assert_awaited_once_with(self.ctx, err)
@@ -365,7 +365,7 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase):
async def test_dont_call_suggestion_tag_sent(self):
"""Should never call command suggestion if tag is already sent."""
self.ctx.message = MagicMock(content="foo")
- self.ctx.invoke = AsyncMock(return_value=True)
+ self.tag.get_command_ctx = AsyncMock(return_value=True)
self.cog.send_command_suggestion = AsyncMock()
await self.cog.try_get_tag(self.ctx)
@@ -385,7 +385,7 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase):
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.tag.get_command_ctx = AsyncMock(return_value=False)
self.cog.send_command_suggestion = AsyncMock()
await self.cog.try_get_tag(self.ctx)
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/recruitment/talentpool/test_review.py b/tests/bot/exts/recruitment/talentpool/test_review.py
index 9ad429ace..f726fccc7 100644
--- a/tests/bot/exts/recruitment/talentpool/test_review.py
+++ b/tests/bot/exts/recruitment/talentpool/test_review.py
@@ -71,6 +71,7 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase):
MockMessage(author=self.bot_user, content="Not a review", created_at=not_too_recent),
MockMessage(author=self.bot_user, content="Not a review", created_at=not_too_recent),
],
+ not_too_recent.timestamp(),
True,
),
@@ -81,6 +82,7 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase):
MockMessage(author=self.bot_user, content="Zig for Helper!", created_at=not_too_recent),
MockMessage(author=self.bot_user, content="Scaleios for Helper!", created_at=not_too_recent),
],
+ not_too_recent.timestamp(),
False,
),
@@ -89,6 +91,7 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase):
[
MockMessage(author=self.bot_user, content="Chrisjl for Helper!", created_at=too_recent),
],
+ too_recent.timestamp(),
False,
),
@@ -100,18 +103,25 @@ class ReviewerTests(unittest.IsolatedAsyncioTestCase):
MockMessage(author=self.bot_user, content="wookie for Helper!", created_at=not_too_recent),
MockMessage(author=self.bot_user, content="Not a review", created_at=not_too_recent),
],
+ not_too_recent.timestamp(),
True,
),
# No messages, so ready.
- ([], True),
+ ([], None, True),
)
- for messages, expected in cases:
+ for messages, last_review_timestamp, expected in cases:
with self.subTest(messages=messages, expected=expected):
self.voting_channel.history = AsyncIterator(messages)
+
+ cache_get_mock = AsyncMock(return_value=last_review_timestamp)
+ self.reviewer.status_cache.get = cache_get_mock
+
res = await self.reviewer.is_ready_for_review()
+
self.assertIs(res, expected)
+ cache_get_mock.assert_called_with("last_vote_date")
@patch("bot.exts.recruitment.talentpool._review.MIN_NOMINATION_TIME", timedelta(days=7))
async def test_get_nomination_to_review(self):
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)
diff --git a/tests/helpers.py b/tests/helpers.py
index 4b980ac21..1a71f210a 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -222,7 +222,7 @@ class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin):
# Create a Member instance to get a realistic Mock of `discord.Member`
-member_data = {'user': 'lemon', 'roles': [1]}
+member_data = {'user': 'lemon', 'roles': [1], 'flags': 2}
state_mock = unittest.mock.MagicMock()
member_instance = discord.Member(data=member_data, guild=guild_instance, state=state_mock)
@@ -479,6 +479,25 @@ class MockContext(CustomMockMixin, unittest.mock.MagicMock):
self.invoked_from_error_handler = kwargs.get('invoked_from_error_handler', False)
+class MockInteraction(CustomMockMixin, unittest.mock.MagicMock):
+ """
+ A MagicMock subclass to mock Interaction objects.
+
+ Instances of this class will follow the specifications of `discord.Interaction`
+ instances. For more information, see the `MockGuild` docstring.
+ """
+
+ def __init__(self, **kwargs) -> None:
+ super().__init__(**kwargs)
+ self.me = kwargs.get('me', MockMember())
+ self.client = kwargs.get('client', MockBot())
+ self.guild = kwargs.get('guild', MockGuild())
+ self.user = kwargs.get('user', MockMember())
+ self.channel = kwargs.get('channel', MockTextChannel())
+ self.message = kwargs.get('message', MockMessage())
+ 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())
@@ -530,6 +549,16 @@ class MockMessage(CustomMockMixin, unittest.mock.MagicMock):
self.channel = kwargs.get('channel', MockTextChannel())
+class MockInteractionMessage(MockMessage):
+ """
+ A MagicMock subclass to mock InteractionMessage objects.
+
+ Instances of this class will follow the specifications of `discord.InteractionMessage` instances. For more
+ information, see the `MockGuild` docstring.
+ """
+ pass
+
+
emoji_data = {'require_colons': True, 'managed': True, 'id': 1, 'name': 'hyperlemon'}
emoji_instance = discord.Emoji(guild=MockGuild(), state=unittest.mock.MagicMock(), data=emoji_data)