diff options
Diffstat (limited to '')
| -rw-r--r-- | tests/bot/exts/backend/test_error_handler.py | 20 | ||||
| -rw-r--r-- | tests/bot/exts/backend/test_security.py | 53 | ||||
| -rw-r--r-- | tests/bot/exts/recruitment/__init__.py | 0 | ||||
| -rw-r--r-- | tests/bot/exts/recruitment/talentpool/test_review.py | 203 | 
4 files changed, 270 insertions, 6 deletions
diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index 7562f6aa8..562c827b9 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -9,7 +9,7 @@ from bot.exts.backend import error_handler  from bot.exts.info.tags import Tags  from bot.exts.moderation.silence import Silence  from bot.utils.checks import InWhitelistCheckFailure -from tests.helpers import MockBot, MockContext, MockGuild, MockRole, MockTextChannel +from tests.helpers import MockBot, MockContext, MockGuild, MockRole, MockTextChannel, MockVoiceChannel  class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): @@ -163,12 +163,11 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase):                  self.assertIsNone(await self.cog.on_command_error(self.ctx, case["error"]))                  case["mock_function_to_call"].assert_awaited_once_with(self.ctx, case["error"].original) -    async def test_error_handler_two_other_errors(self): -        """Should call `handle_unexpected_error` if error is `MaxConcurrencyReached` or `ExtensionError`.""" +    async def test_error_handler_unexpected_errors(self): +        """Should call `handle_unexpected_error` if error is `ExtensionError`."""          self.cog.handle_unexpected_error = AsyncMock()          errs = ( -            errors.MaxConcurrencyReached(1, MagicMock()), -            errors.ExtensionError(name="foo") +            errors.ExtensionError(name="foo"),          )          for err in errs: @@ -192,7 +191,16 @@ class TrySilenceTests(unittest.IsolatedAsyncioTestCase):          self.bot = MockBot()          self.silence = Silence(self.bot)          self.bot.get_command.return_value = self.silence.silence -        self.ctx = MockContext(bot=self.bot) + +        # Use explicit mock channels so that discord.utils.get doesn't think +        # guild.text_channels is an async iterable due to the MagicMock having +        # a __aiter__ attr. +        guild_overrides = { +            "text_channels": [MockTextChannel(), MockTextChannel()], +            "voice_channels": [MockVoiceChannel(), MockVoiceChannel()], +        } +        self.guild = MockGuild(**guild_overrides) +        self.ctx = MockContext(bot=self.bot, guild=self.guild)          self.cog = error_handler.ErrorHandler(self.bot)      async def test_try_silence_context_invoked_from_error_handler(self): diff --git a/tests/bot/exts/backend/test_security.py b/tests/bot/exts/backend/test_security.py new file mode 100644 index 000000000..c3985c609 --- /dev/null +++ b/tests/bot/exts/backend/test_security.py @@ -0,0 +1,53 @@ +import unittest + +from discord.ext.commands import NoPrivateMessage + +from bot.exts.backend import security +from tests.helpers import MockBot, MockContext + + +class SecurityCogTests(unittest.TestCase): +    """Tests the `Security` cog.""" + +    def setUp(self): +        """Attach an instance of the cog to the class for tests.""" +        self.bot = MockBot() +        self.cog = security.Security(self.bot) +        self.ctx = MockContext() + +    def test_check_additions(self): +        """The cog should add its checks after initialization.""" +        self.bot.check.assert_any_call(self.cog.check_on_guild) +        self.bot.check.assert_any_call(self.cog.check_not_bot) + +    def test_check_not_bot_returns_false_for_humans(self): +        """The bot check should return `True` when invoked with human authors.""" +        self.ctx.author.bot = False +        self.assertTrue(self.cog.check_not_bot(self.ctx)) + +    def test_check_not_bot_returns_true_for_robots(self): +        """The bot check should return `False` when invoked with robotic authors.""" +        self.ctx.author.bot = True +        self.assertFalse(self.cog.check_not_bot(self.ctx)) + +    def test_check_on_guild_raises_when_outside_of_guild(self): +        """When invoked outside of a guild, `check_on_guild` should cause an error.""" +        self.ctx.guild = None + +        with self.assertRaises(NoPrivateMessage, msg="This command cannot be used in private messages."): +            self.cog.check_on_guild(self.ctx) + +    def test_check_on_guild_returns_true_inside_of_guild(self): +        """When invoked inside of a guild, `check_on_guild` should return `True`.""" +        self.ctx.guild = "lemon's lemonade stand" +        self.assertTrue(self.cog.check_on_guild(self.ctx)) + + +class SecurityCogLoadTests(unittest.IsolatedAsyncioTestCase): +    """Tests loading the `Security` cog.""" + +    async def test_security_cog_load(self): +        """Setup of the extension should call add_cog.""" +        bot = MockBot() +        await security.setup(bot) +        bot.add_cog.assert_awaited_once() diff --git a/tests/bot/exts/recruitment/__init__.py b/tests/bot/exts/recruitment/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/bot/exts/recruitment/__init__.py diff --git a/tests/bot/exts/recruitment/talentpool/test_review.py b/tests/bot/exts/recruitment/talentpool/test_review.py new file mode 100644 index 000000000..ed9b66e12 --- /dev/null +++ b/tests/bot/exts/recruitment/talentpool/test_review.py @@ -0,0 +1,203 @@ +import unittest +from datetime import datetime, timedelta, timezone +from unittest.mock import Mock, patch + +from bot.exts.recruitment.talentpool import _review +from tests.helpers import MockBot, MockMember, MockMessage, MockTextChannel + + +class AsyncIterator: +    """Normal->Async iterator helper.""" + +    def __init__(self, seq): +        self.iter = iter(seq) + +    def __aiter__(self): +        return self + +    # Allows it to be used to mock the discord TextChannel.history function +    def __call__(self): +        return self + +    async def __anext__(self): +        try: +            return next(self.iter) +        except StopIteration: +            raise StopAsyncIteration + + +def nomination( +    inserted_at: datetime, +    num_entries: int, +    reviewed: bool = False, +    id: int | None = None +) -> tuple[int, dict]: +    return ( +        id or MockMember().id, +        {"inserted_at": inserted_at.isoformat(), "entries": [Mock() for _ in range(num_entries)], "reviewed": reviewed}, +    ) + + +class ReviewerTests(unittest.IsolatedAsyncioTestCase): +    """Tests for the talentpool reviewer.""" + +    def setUp(self): +        self.bot_user = MockMember(bot=True) +        self.bot = MockBot(user=self.bot_user) + +        self.voting_channel = MockTextChannel() +        self.bot.get_channel = Mock(return_value=self.voting_channel) + +        self.pool = Mock(name="MockTalentPool") +        self.reviewer = _review.Reviewer(self.bot, self.pool) + +    @patch("bot.exts.recruitment.talentpool._review.MAX_ONGOING_REVIEWS", 3) +    @patch("bot.exts.recruitment.talentpool._review.MIN_REVIEW_INTERVAL", timedelta(days=1)) +    async def test_is_ready_for_review(self): +        """Tests for the `is_ready_for_review` function.""" +        too_recent = datetime.now(timezone.utc) - timedelta(hours=1) +        not_too_recent = datetime.now(timezone.utc) - timedelta(days=7) +        cases = ( +            # Only one review, and not too recent, so ready. +            ( +                [ +                    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), +                    MockMessage(author=self.bot_user, content="Not a review", created_at=not_too_recent), +                ], +                True, +            ), + +            # Three reviews, so not ready. +            ( +                [ +                    MockMessage(author=self.bot_user, content="Chrisjl for Helper!", created_at=not_too_recent), +                    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), +                ], +                False, +            ), + +            # Only one review, but too recent, so not ready. +            ( +                [ +                    MockMessage(author=self.bot_user, content="Chrisjl for Helper!", created_at=too_recent), +                ], +                False, +            ), + +            # Only two reviews, and not too recent, so ready. +            ( +                [ +                    MockMessage(author=self.bot_user, content="Not a review", created_at=too_recent), +                    MockMessage(author=self.bot_user, content="wookie for Helper!", created_at=not_too_recent), +                    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), +                ], +                True, +            ), + +            # No messages, so ready. +            ([], True), +        ) + +        for messages, expected in cases: +            with self.subTest(messages=messages, expected=expected): +                self.voting_channel.history = AsyncIterator(messages) +                res = await self.reviewer.is_ready_for_review() +                self.assertIs(res, expected) + +    @patch("bot.exts.recruitment.talentpool._review.MIN_NOMINATION_TIME", timedelta(days=7)) +    async def test_get_user_for_review(self): +        """Test get_user_for_review function.""" +        now = datetime.now(timezone.utc) + +        # Each case contains a list of nominations, followed by the index in that list +        # of the one that should be selected, or None if None should be returned +        cases = [ +            # One nomination, too recent so don't send. +            ( +                [ +                    nomination(now - timedelta(days=1), 5), +                ], +                None, +            ), + +            # First one has most entries so should be returned. +            ( +                [ +                    nomination(now - timedelta(days=10), 6), +                    nomination(now - timedelta(days=10), 5), +                    nomination(now - timedelta(days=9), 5), +                    nomination(now - timedelta(days=11), 5), +                ], +                0, +            ), + +            # Same number of entries so oldest (second) should be returned. +            ( +                [ +                    nomination(now - timedelta(days=1), 2), +                    nomination(now - timedelta(days=80), 2), +                    nomination(now - timedelta(days=79), 2), +                ], +                1, +            ), +        ] + +        for (case_num, (nominations, expected)) in enumerate(cases, 1): +            nomination_dict = dict(nominations) + +            with self.subTest(case_num=case_num): +                self.pool.cache = nomination_dict +                res = await self.reviewer.get_user_for_review() + +                if expected is None: +                    self.assertIsNone(res) +                else: +                    self.assertEqual(res, nominations[expected][0]) + +    @patch("bot.exts.recruitment.talentpool._review.MIN_NOMINATION_TIME", timedelta(days=0)) +    async def test_get_user_for_review_order(self): +        now = datetime.now(timezone.utc) + +        # Each case in cases is a list of nominations in the order they should be chosen from first to last +        cases = [ +            [ +                nomination(now - timedelta(days=10), 3, id=1), +                nomination(now - timedelta(days=50), 2, id=2), +                nomination(now - timedelta(days=100), 1, id=3), +            ], +            [ +                nomination(now - timedelta(days=100), 2, id=1), +                nomination(now - timedelta(days=10), 3, id=2), +                nomination(now - timedelta(days=80), 1, id=3), +                nomination(now - timedelta(days=79), 1, id=4), +                nomination(now - timedelta(days=10), 2, id=5), +            ], +            [ +                nomination(now - timedelta(days=200), 8, id=1), +                nomination(now - timedelta(days=359), 4, id=2), +                nomination(now - timedelta(days=230), 5, id=3), +                nomination(now - timedelta(days=331), 3, id=4), +                nomination(now - timedelta(days=113), 5, id=5), +                nomination(now - timedelta(days=186), 3, id=6), +                nomination(now - timedelta(days=272), 2, id=7), +                nomination(now - timedelta(days=30), 4, id=8), +                nomination(now - timedelta(days=198), 2, id=9), +                nomination(now - timedelta(days=270), 1, id=10), +                nomination(now - timedelta(days=140), 1, id=11), +                nomination(now - timedelta(days=19), 2, id=12), +                nomination(now - timedelta(days=30), 1, id=13), +            ] +        ] + +        for case_num, case in enumerate(cases, 1): +            with self.subTest(case_num=case_num): +                for i in range(len(case)): +                    with self.subTest(nomination_num=i+1): +                        sub_case = dict(case[i:]) +                        self.pool.cache = sub_case + +                        res = await self.reviewer.get_user_for_review() +                        self.assertEqual(res, case[i][0])  |