diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/bot/exts/moderation/infraction/test_utils.py | 359 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/test_silence.py | 10 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/test_slowmode.py | 10 | ||||
| -rw-r--r-- | tests/bot/utils/test_checks.py | 44 | 
4 files changed, 394 insertions, 29 deletions
| diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py new file mode 100644 index 000000000..5b62463e0 --- /dev/null +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -0,0 +1,359 @@ +import unittest +from collections import namedtuple +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.constants import Colours, Icons +from bot.exts.moderation.infraction import _utils as utils +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_post_user(self): +        """Should POST a new user and return the response if successful or otherwise send an error message.""" +        user = MockUser(discriminator=5678, id=1234, name="Test user") +        not_user = MagicMock(discriminator=3333, id=5678, name="Wrong user") +        test_cases = [ +            { +                "user": user, +                "post_result": "bar", +                "raise_error": None, +                "payload": { +                    "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": { +                    "discriminator": 0, +                    "id": self.member.id, +                    "in_guild": False, +                    "name": "Name unknown", +                    "roles": [] +                } +            }, +            { +                "user": not_user, +                "post_result": "bar", +                "raise_error": None, +                "payload": { +                    "discriminator": not_user.discriminator, +                    "id": not_user.id, +                    "in_guild": False, +                    "name": not_user.name, +                    "roles": [] +                } +            } +        ] + +        for case in test_cases: +            user = case["user"] +            post_result = case["post_result"] +            raise_error = case["raise_error"] +            payload = case["payload"] + +            with self.subTest(user=user, post_result=post_result, raise_error=raise_error, payload=payload): +                self.bot.api_client.post.reset_mock(side_effect=True) +                self.ctx.bot.api_client.post.return_value = post_result + +                self.ctx.bot.api_client.post.side_effect = raise_error + +                result = await utils.post_user(self.ctx, user) + +                if raise_error: +                    self.assertIsNone(result) +                    self.ctx.send.assert_awaited_once() +                    self.assertIn(str(raise_error.status), self.ctx.send.call_args[0][0]) +                else: +                    self.assertEqual(result, post_result) +                    self.bot.api_client.post.assert_awaited_once_with("bot/users", json=payload) + +    async def test_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_case = namedtuple("test_case", ["get_return_value", "expected_output", "infraction_nr", "send_msg"]) +        test_cases = [ +            test_case([], None, None, True), +            test_case([{"id": 123987}], {"id": 123987}, "123987", False), +            test_case([{"id": 123987}], {"id": 123987}, "123987", True) +        ] + +        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", send_msg=case.send_msg) +                self.assertEqual(result, case.expected_output) +                self.bot.api_client.get.assert_awaited_once_with("bot/infractions", params=params) + +                if case.send_msg and case.get_return_value: +                    self.ctx.send.assert_awaited_once() +                    sent_message = self.ctx.send.call_args[0][0] +                    self.assertIn(case.infraction_nr, sent_message) +                    self.assertIn("ban", sent_message) +                else: +                    self.ctx.send.assert_not_awaited() + +    @patch("bot.exts.moderation.infraction._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 +            }, +            { +                "args": (self.user, "mute", None, "foo bar" * 4000, Icons.defcon_denied), +                "expected_output": Embed( +                    title=utils.INFRACTION_TITLE, +                    description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( +                        type="Mute", +                        expires="N/A", +                        reason="foo bar" * 4000 +                    )[:2045] + "...", +                    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": True +            } +        ] + +        for case in test_cases: +            with self.subTest(args=case["args"], expected=case["expected_output"], send=case["send_result"]): +                send_private_embed_mock.reset_mock() + +                send_private_embed_mock.return_value = case["send_result"] +                result = await utils.notify_infraction(*case["args"]) + +                self.assertEqual(case["send_result"], result) + +                embed = send_private_embed_mock.call_args[0][1] + +                self.assertEqual(embed.to_dict(), case["expected_output"].to_dict()) + +                send_private_embed_mock.assert_awaited_once_with(case["args"][0], embed) + +    @patch("bot.exts.moderation.infraction._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_case = namedtuple("test_case", ["args", "icon", "send_result"]) +        test_cases = [ +            test_case((self.user, "Test title", "Example content"), Icons.user_verified, True), +            test_case((self.user, "Test title", "Example content", Icons.user_update), Icons.user_update, False) +        ] + +        for case in test_cases: +            expected = Embed( +                description="Example content", +                colour=Colours.soft_green +            ).set_author( +                name="Test title", +                icon_url=case.icon +            ) + +            with self.subTest(args=case.args, expected=expected): +                send_private_embed_mock.reset_mock() + +                send_private_embed_mock.return_value = case.send_result + +                result = await utils.notify_pardon(*case.args) +                self.assertEqual(case.send_result, 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(case.args[0], embed) + +    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_case = namedtuple("test_case", ["expected_output", "raised_exception"]) +        test_cases = [ +            test_case(True, None), +            test_case(False, HTTPException(AsyncMock(), AsyncMock())), +            test_case(False, Forbidden(AsyncMock(), AsyncMock())), +            test_case(False, NotFound(AsyncMock(), AsyncMock())) +        ] + +        for case in test_cases: +            with self.subTest(expected=case.expected_output, raised=case.raised_exception): +                self.user.send.reset_mock(side_effect=True) +                self.user.send.side_effect = case.raised_exception + +                result = await utils.send_private_embed(self.user, embed) + +                self.assertEqual(result, case.expected_output) +                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.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.exts.moderation.infraction._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.exts.moderation.infraction._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.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) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 8c4fb764a..e2d44c637 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -253,9 +253,11 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase):          self.cog.cog_unload()          asyncio_mock.create_task.assert_not_called() -    @mock.patch("bot.exts.moderation.silence.with_role_check") +    @mock.patch("discord.ext.commands.has_any_role")      @mock.patch("bot.exts.moderation.silence.MODERATION_ROLES", new=(1, 2, 3)) -    def test_cog_check(self, role_check): +    async def test_cog_check(self, role_check):          """Role check is called with `MODERATION_ROLES`""" -        self.cog.cog_check(self.ctx) -        role_check.assert_called_once_with(self.ctx, *(1, 2, 3)) +        role_check.return_value.predicate = mock.AsyncMock() +        await self.cog.cog_check(self.ctx) +        role_check.assert_called_once_with(*(1, 2, 3)) +        role_check.return_value.predicate.assert_awaited_once_with(self.ctx) diff --git a/tests/bot/exts/moderation/test_slowmode.py b/tests/bot/exts/moderation/test_slowmode.py index e90394ab9..dad751e0d 100644 --- a/tests/bot/exts/moderation/test_slowmode.py +++ b/tests/bot/exts/moderation/test_slowmode.py @@ -103,9 +103,11 @@ class SlowmodeTests(unittest.IsolatedAsyncioTestCase):              f'{Emojis.check_mark} The slowmode delay for #meta has been reset to 0 seconds.'          ) -    @mock.patch("bot.exts.moderation.slowmode.with_role_check") +    @mock.patch("bot.exts.moderation.slowmode.has_any_role")      @mock.patch("bot.exts.moderation.slowmode.MODERATION_ROLES", new=(1, 2, 3)) -    def test_cog_check(self, role_check): +    async def test_cog_check(self, role_check):          """Role check is called with `MODERATION_ROLES`""" -        self.cog.cog_check(self.ctx) -        role_check.assert_called_once_with(self.ctx, *(1, 2, 3)) +        role_check.return_value.predicate = mock.AsyncMock() +        await self.cog.cog_check(self.ctx) +        role_check.assert_called_once_with(*(1, 2, 3)) +        role_check.return_value.predicate.assert_awaited_once_with(self.ctx) diff --git a/tests/bot/utils/test_checks.py b/tests/bot/utils/test_checks.py index de72e5748..883465e0b 100644 --- a/tests/bot/utils/test_checks.py +++ b/tests/bot/utils/test_checks.py @@ -1,48 +1,50 @@  import unittest  from unittest.mock import MagicMock +from discord import DMChannel +  from bot.utils import checks  from bot.utils.checks import InWhitelistCheckFailure  from tests.helpers import MockContext, MockRole -class ChecksTests(unittest.TestCase): +class ChecksTests(unittest.IsolatedAsyncioTestCase):      """Tests the check functions defined in `bot.checks`."""      def setUp(self):          self.ctx = MockContext() -    def test_with_role_check_without_guild(self): -        """`with_role_check` returns `False` if `Context.guild` is None.""" -        self.ctx.guild = None -        self.assertFalse(checks.with_role_check(self.ctx)) +    async def test_has_any_role_check_without_guild(self): +        """`has_any_role_check` returns `False` for non-guild channels.""" +        self.ctx.channel = MagicMock(DMChannel) +        self.assertFalse(await checks.has_any_role_check(self.ctx)) -    def test_with_role_check_without_required_roles(self): -        """`with_role_check` returns `False` if `Context.author` lacks the required role.""" +    async def test_has_any_role_check_without_required_roles(self): +        """`has_any_role_check` returns `False` if `Context.author` lacks the required role."""          self.ctx.author.roles = [] -        self.assertFalse(checks.with_role_check(self.ctx)) +        self.assertFalse(await checks.has_any_role_check(self.ctx)) -    def test_with_role_check_with_guild_and_required_role(self): -        """`with_role_check` returns `True` if `Context.author` has the required role.""" +    async def test_has_any_role_check_with_guild_and_required_role(self): +        """`has_any_role_check` returns `True` if `Context.author` has the required role."""          self.ctx.author.roles.append(MockRole(id=10)) -        self.assertTrue(checks.with_role_check(self.ctx, 10)) +        self.assertTrue(await checks.has_any_role_check(self.ctx, 10)) -    def test_without_role_check_without_guild(self): -        """`without_role_check` should return `False` when `Context.guild` is None.""" -        self.ctx.guild = None -        self.assertFalse(checks.without_role_check(self.ctx)) +    async def test_has_no_roles_check_without_guild(self): +        """`has_no_roles_check` should return `False` when `Context.guild` is None.""" +        self.ctx.channel = MagicMock(DMChannel) +        self.assertFalse(await checks.has_no_roles_check(self.ctx)) -    def test_without_role_check_returns_false_with_unwanted_role(self): -        """`without_role_check` returns `False` if `Context.author` has unwanted role.""" +    async def test_has_no_roles_check_returns_false_with_unwanted_role(self): +        """`has_no_roles_check` returns `False` if `Context.author` has unwanted role."""          role_id = 42          self.ctx.author.roles.append(MockRole(id=role_id)) -        self.assertFalse(checks.without_role_check(self.ctx, role_id)) +        self.assertFalse(await checks.has_no_roles_check(self.ctx, role_id)) -    def test_without_role_check_returns_true_without_unwanted_role(self): -        """`without_role_check` returns `True` if `Context.author` does not have unwanted role.""" +    async def test_has_no_roles_check_returns_true_without_unwanted_role(self): +        """`has_no_roles_check` returns `True` if `Context.author` does not have unwanted role."""          role_id = 42          self.ctx.author.roles.append(MockRole(id=role_id)) -        self.assertTrue(checks.without_role_check(self.ctx, role_id + 10)) +        self.assertTrue(await checks.has_no_roles_check(self.ctx, role_id + 10))      def test_in_whitelist_check_correct_channel(self):          """`in_whitelist_check` returns `True` if `Context.channel.id` is in the channel list.""" | 
