aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/bot/cogs/moderation/test_utils.py363
1 files changed, 363 insertions, 0 deletions
diff --git a/tests/bot/cogs/moderation/test_utils.py b/tests/bot/cogs/moderation/test_utils.py
new file mode 100644
index 000000000..248adbcb8
--- /dev/null
+++ b/tests/bot/cogs/moderation/test_utils.py
@@ -0,0 +1,363 @@
+import unittest
+from datetime import datetime
+from unittest.mock import AsyncMock, MagicMock, call, patch
+
+from discord import Embed, Forbidden, HTTPException, NotFound
+
+from bot.api import ResponseCodeError
+from bot.cogs.moderation import utils
+from bot.constants import Colours, Icons
+from tests.helpers import MockBot, MockContext, MockMember, MockUser
+
+
+class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase):
+ """Tests Moderation utils."""
+
+ def setUp(self):
+ self.bot = MockBot()
+ self.member = MockMember(id=1234)
+ self.user = MockUser(id=1234)
+ self.ctx = MockContext(bot=self.bot, author=self.member)
+
+ async def test_user_get_active_infraction(self):
+ """
+ Should request the API for active infractions and return infraction if the user has one or `None` otherwise.
+
+ A message should be sent to the context indicating a user already has an infraction, if that's the case.
+ """
+ test_cases = [
+ {
+ "get_return_value": [],
+ "expected_output": None,
+ "infraction_nr": None
+ },
+ {
+ "get_return_value": [{"id": 123987}],
+ "expected_output": {"id": 123987},
+ "infraction_nr": "123987"
+ }
+ ]
+
+ for case in test_cases:
+ with self.subTest(return_value=case["get_return_value"], expected=case["expected_output"]):
+ self.bot.api_client.get.reset_mock()
+ self.ctx.send.reset_mock()
+
+ params = {
+ "active": "true",
+ "type": "ban",
+ "user__id": str(self.member.id)
+ }
+
+ self.bot.api_client.get.return_value = case["get_return_value"]
+
+ result = await utils.get_active_infraction(self.ctx, self.member, "ban")
+ self.assertEqual(result, case["expected_output"])
+ self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params=params)
+
+ if result:
+ self.assertTrue(case["infraction_nr"] in self.ctx.send.call_args[0][0])
+ self.assertTrue("ban" in self.ctx.send.call_args[0][0])
+
+ @patch("bot.cogs.moderation.utils.send_private_embed")
+ async def test_notify_infraction(self, send_private_embed_mock):
+ """
+ Should send an embed of a certain format as a DM and return `True` if DM successful.
+
+ Appealable infractions should have the appeal message in the embed's footer.
+ """
+ test_cases = [
+ {
+ "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"),
+ "expected_output": Embed(
+ title=utils.INFRACTION_TITLE,
+ description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
+ type="Ban",
+ expires="2020-02-26 09:20 (23 hours and 59 minutes)",
+ reason="No reason provided."
+ ),
+ colour=Colours.soft_red,
+ url=utils.RULES_URL
+ ).set_author(
+ name=utils.INFRACTION_AUTHOR_NAME,
+ url=utils.RULES_URL,
+ icon_url=Icons.token_removed
+ ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER),
+ "send_result": True
+ },
+ {
+ "args": (self.user, "warning", None, "Test reason."),
+ "expected_output": Embed(
+ title=utils.INFRACTION_TITLE,
+ description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
+ type="Warning",
+ expires="N/A",
+ reason="Test reason."
+ ),
+ colour=Colours.soft_red,
+ url=utils.RULES_URL
+ ).set_author(
+ name=utils.INFRACTION_AUTHOR_NAME,
+ url=utils.RULES_URL,
+ icon_url=Icons.token_removed
+ ),
+ "send_result": False
+ },
+ {
+ "args": (self.user, "note", None, None, Icons.defcon_denied),
+ "expected_output": Embed(
+ title=utils.INFRACTION_TITLE,
+ description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
+ type="Note",
+ expires="N/A",
+ reason="No reason provided."
+ ),
+ colour=Colours.soft_red,
+ url=utils.RULES_URL
+ ).set_author(
+ name=utils.INFRACTION_AUTHOR_NAME,
+ url=utils.RULES_URL,
+ icon_url=Icons.defcon_denied
+ ),
+ "send_result": False
+ },
+ {
+ "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied),
+ "expected_output": Embed(
+ title=utils.INFRACTION_TITLE,
+ description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format(
+ type="Mute",
+ expires="2020-02-26 09:20 (23 hours and 59 minutes)",
+ reason="Test"
+ ),
+ colour=Colours.soft_red,
+ url=utils.RULES_URL
+ ).set_author(
+ name=utils.INFRACTION_AUTHOR_NAME,
+ url=utils.RULES_URL,
+ icon_url=Icons.defcon_denied
+ ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER),
+ "send_result": False
+ }
+ ]
+
+ for case in test_cases:
+ args = case["args"]
+ expected = case["expected_output"]
+ send = case["send_result"]
+
+ with self.subTest(args=args, expected=expected, send=send):
+ send_private_embed_mock.reset_mock()
+
+ send_private_embed_mock.return_value = send
+ result = await utils.notify_infraction(*args)
+
+ self.assertEqual(send, result)
+
+ embed = send_private_embed_mock.call_args[0][1]
+
+ self.assertEqual(embed.to_dict(), expected.to_dict())
+
+ send_private_embed_mock.assert_awaited_once_with(args[0], embed)
+
+ @patch("bot.cogs.moderation.utils.send_private_embed")
+ async def test_notify_pardon(self, send_private_embed_mock):
+ """Should send an embed of a certain format as a DM and return `True` if DM successful."""
+ test_cases = [
+ {
+ "args": (self.user, "Test title", "Example content"),
+ "icon": Icons.user_verified,
+ "send_result": True
+ },
+ {
+ "args": (self.user, "Test title", "Example content", Icons.user_update),
+ "icon": Icons.user_update,
+ "send_result": False
+ }
+ ]
+
+ for case in test_cases:
+ args = case["args"]
+ send = case["send_result"]
+
+ expected = Embed(
+ description="Example content",
+ colour=Colours.soft_green
+ ).set_author(
+ name="Test title",
+ icon_url=case["icon"]
+ )
+
+ with self.subTest(args=args, expected=expected):
+ send_private_embed_mock.reset_mock()
+
+ send_private_embed_mock.return_value = send
+
+ result = await utils.notify_pardon(*args)
+ self.assertEqual(send, result)
+
+ embed = send_private_embed_mock.call_args[0][1]
+ self.assertEqual(embed.to_dict(), expected.to_dict())
+
+ send_private_embed_mock.assert_awaited_once_with(args[0], embed)
+
+ async def test_post_user(self):
+ """Should POST a new user and return the response if successful or otherwise send an error message."""
+ user = MockUser(avatar="abc", discriminator=5678, id=1234, name="Test user")
+ test_cases = [
+ {
+ "user": user,
+ "post_result": "bar",
+ "raise_error": None,
+ "payload": {
+ "avatar_hash": "abc",
+ "discriminator": 5678,
+ "id": self.user.id,
+ "in_guild": False,
+ "name": "Test user",
+ "roles": []
+ }
+ },
+ {
+ "user": self.member,
+ "post_result": "foo",
+ "raise_error": ResponseCodeError(MagicMock(status=400), "foo"),
+ "payload": {
+ "avatar_hash": 0,
+ "discriminator": 0,
+ "id": self.member.id,
+ "in_guild": False,
+ "name": "Name unknown",
+ "roles": []
+ }
+ }
+ ]
+
+ for case in test_cases:
+ test_user = case["user"]
+ expected = case["post_result"]
+ error = case["raise_error"]
+ payload = case["payload"]
+
+ with self.subTest(user=test_user, result=expected, error=error, payload=payload):
+ self.bot.api_client.post.reset_mock(side_effect=True)
+ self.ctx.bot.api_client.post.return_value = expected
+
+ self.ctx.bot.api_client.post.side_effect = error
+
+ result = await utils.post_user(self.ctx, test_user)
+
+ if error:
+ self.assertIsNone(result)
+ else:
+ self.assertEqual(result, expected)
+
+ if not error:
+ self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload)
+ else:
+ self.ctx.send.assert_awaited_once()
+ self.assertTrue(str(error.status) in self.ctx.send.call_args[0][0])
+
+ async def test_send_private_embed(self):
+ """Should DM the user and return `True` on success or `False` on failure."""
+ embed = Embed(title="Test", description="Test val")
+
+ test_cases = [
+ {
+ "expected_output": True,
+ "raised_exception": None
+ },
+ {
+ "expected_output": False,
+ "raised_exception": HTTPException(AsyncMock(), AsyncMock())
+ },
+ {
+ "expected_output": False,
+ "raised_exception": Forbidden(AsyncMock(), AsyncMock())
+ },
+ {
+ "expected_output": False,
+ "raised_exception": NotFound(AsyncMock(), AsyncMock())
+ }
+ ]
+
+ for case in test_cases:
+ expected = case["expected_output"]
+ raised = case["raised_exception"]
+
+ with self.subTest(expected=expected, raised=raised):
+ self.user.send.reset_mock(side_effect=True)
+ self.user.send.side_effect = raised
+
+ result = await utils.send_private_embed(self.user, embed)
+
+ self.assertEqual(result, expected)
+ if expected:
+ self.user.send.assert_awaited_once_with(embed=embed)
+
+
+class TestPostInfraction(unittest.IsolatedAsyncioTestCase):
+ """Tests for the `post_infraction` function."""
+
+ def setUp(self):
+ self.bot = MockBot()
+ self.member = MockMember(id=1234)
+ self.user = MockUser(id=1234)
+ self.ctx = MockContext(bot=self.bot, author=self.member)
+
+ async def test_normal_post_infraction(self):
+ """Should return response from POST request if there are no errors."""
+ now = datetime.now()
+ payload = {
+ "actor": self.ctx.message.author.id,
+ "hidden": True,
+ "reason": "Test reason",
+ "type": "ban",
+ "user": self.member.id,
+ "active": False,
+ "expires_at": now.isoformat()
+ }
+
+ self.ctx.bot.api_client.post.return_value = "foo"
+ actual = await utils.post_infraction(self.ctx, self.member, "ban", "Test reason", now, True, False)
+
+ self.assertEqual(actual, "foo")
+ self.ctx.bot.api_client.post.assert_awaited_once_with("bot/infractions", json=payload)
+
+ async def test_unknown_error_post_infraction(self):
+ """Should send an error message to chat when a non-400 error occurs."""
+ self.ctx.bot.api_client.post.side_effect = ResponseCodeError(AsyncMock(), AsyncMock())
+ self.ctx.bot.api_client.post.side_effect.status = 500
+
+ actual = await utils.post_infraction(self.ctx, self.user, "ban", "Test reason")
+ self.assertIsNone(actual)
+
+ self.assertTrue("500" in self.ctx.send.call_args[0][0])
+
+ @patch("bot.cogs.moderation.utils.post_user", return_value=None)
+ async def test_user_not_found_none_post_infraction(self, post_user_mock):
+ """Should abort and return `None` when a new user fails to be posted."""
+ self.bot.api_client.post.side_effect = ResponseCodeError(MagicMock(status=400), {"user": "foo"})
+
+ actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason")
+ self.assertIsNone(actual)
+ post_user_mock.assert_awaited_once_with(self.ctx, self.user)
+
+ @patch("bot.cogs.moderation.utils.post_user", return_value="bar")
+ async def test_first_fail_second_success_user_post_infraction(self, post_user_mock):
+ """Should post the user if they don't exist, POST infraction again, and return the response if successful."""
+ payload = {
+ "actor": self.ctx.message.author.id,
+ "hidden": False,
+ "reason": "Test reason",
+ "type": "mute",
+ "user": self.user.id,
+ "active": True
+ }
+
+ self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"]
+
+ actual = await utils.post_infraction(self.ctx, self.user, "mute", "Test reason")
+ self.assertEqual(actual, "foo")
+ self.bot.api_client.post.assert_has_awaits([call("bot/infractions", json=payload)] * 2)
+ post_user_mock.assert_awaited_once_with(self.ctx, self.user)