diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/bot/cogs/test_duck_pond.py | 200 |
1 files changed, 128 insertions, 72 deletions
diff --git a/tests/bot/cogs/test_duck_pond.py b/tests/bot/cogs/test_duck_pond.py index 8f0c4f068..b801e86f1 100644 --- a/tests/bot/cogs/test_duck_pond.py +++ b/tests/bot/cogs/test_duck_pond.py @@ -72,8 +72,7 @@ class DuckPondTests(base.LoggingTestCase): self.assertEqual(len(log_watcher.records), 1) - [record] = log_watcher.records - self.assertEqual(record.message, f"Failed to fetch webhook with id `{self.cog.webhook_id}`") + record = log_watcher.records[0] self.assertEqual(record.levelno, logging.ERROR) def test_is_staff_returns_correct_values_based_on_instance_passed(self): @@ -99,15 +98,15 @@ class DuckPondTests(base.LoggingTestCase): ( "No green check mark reactions", helpers.MockMessage(reactions=[ - helpers.MockReaction(emoji=self.unicode_duck_emoji), - helpers.MockReaction(emoji=self.thumbs_up_emoji) + helpers.MockReaction(emoji=self.unicode_duck_emoji, users=[self.bot.user]), + helpers.MockReaction(emoji=self.thumbs_up_emoji, users=[self.bot.user]) ]), False ), ( "Green check mark reaction, but not from our bot", helpers.MockMessage(reactions=[ - helpers.MockReaction(emoji=self.unicode_duck_emoji), + helpers.MockReaction(emoji=self.unicode_duck_emoji, users=[self.bot.user]), helpers.MockReaction(emoji=self.checkmark_emoji, users=[self.staff_member]) ]), False @@ -115,7 +114,7 @@ class DuckPondTests(base.LoggingTestCase): ( "Green check mark reaction, with one from the bot", helpers.MockMessage(reactions=[ - helpers.MockReaction(emoji=self.unicode_duck_emoji), + helpers.MockReaction(emoji=self.unicode_duck_emoji, users=[self.bot.user]), helpers.MockReaction(emoji=self.checkmark_emoji, users=[self.staff_member, self.bot.user]) ]), True @@ -160,8 +159,7 @@ class DuckPondTests(base.LoggingTestCase): self.assertEqual(len(log_watcher.records), 1) - [record] = log_watcher.records - self.assertEqual(record.message, "Failed to send a message to the Duck Pool webhook") + record = log_watcher.records[0] self.assertEqual(record.levelno, logging.ERROR) def _get_reaction( @@ -250,10 +248,12 @@ class DuckPondTests(base.LoggingTestCase): # A staffer with multiple duck reactions only counts once ( "Two different duck reactions from the same staffer", - helpers.MockMessage(reactions=[ - helpers.MockReaction(emoji=self.duck_pond_emoji, users=[self.staff_member]), - helpers.MockReaction(emoji=self.unicode_duck_emoji, users=[self.staff_member]), - ]), + helpers.MockMessage( + reactions=[ + helpers.MockReaction(emoji=self.duck_pond_emoji, users=[self.staff_member]), + helpers.MockReaction(emoji=self.unicode_duck_emoji, users=[self.staff_member]), + ] + ), 1 ), # A non-string emoji does not count (to test the `isinstance(reaction.emoji, str)` elif) @@ -265,10 +265,12 @@ class DuckPondTests(base.LoggingTestCase): # We correctly sum when multiple reactions are provided. ( "Duckpond Duck Reaction from 3 staffers + 2 non-staffers", - helpers.MockMessage(reactions=[ - self._get_reaction(emoji=self.duck_pond_emoji, staff=3, nonstaff=2), - self._get_reaction(emoji=self.unicode_duck_emoji, staff=4, nonstaff=9), - ]), + helpers.MockMessage( + reactions=[ + self._get_reaction(emoji=self.duck_pond_emoji, staff=3, nonstaff=2), + self._get_reaction(emoji=self.unicode_duck_emoji, staff=4, nonstaff=9), + ] + ), 3 + 4 ), ) @@ -279,8 +281,8 @@ class DuckPondTests(base.LoggingTestCase): self.assertEqual(expected_count, actual_count) @helpers.async_test - async def test_relay_message_to_duck_pond_correctly_relays_content_and_attachments(self): - """The `relay_message_to_duck_pond` method should correctly relay message content and attachments.""" + async def test_relay_message_correctly_relays_content_and_attachments(self): + """The `relay_message` method should correctly relay message content and attachments.""" send_webhook_path = f"{MODULE_PATH}.DuckPond.send_webhook" send_attachments_path = f"{MODULE_PATH}.send_attachments" @@ -297,41 +299,47 @@ class DuckPondTests(base.LoggingTestCase): with patch(send_webhook_path, new_callable=helpers.AsyncMock) as send_webhook: with patch(send_attachments_path, new_callable=helpers.AsyncMock) as send_attachments: with self.subTest(clean_content=message.clean_content, attachments=message.attachments): - await self.cog.relay_message_to_duck_pond(message) + await self.cog.relay_message(message) self.assertEqual(expect_webhook_call, send_webhook.called) self.assertEqual(expect_attachment_call, send_attachments.called) message.add_reaction.assert_called_once_with(self.checkmark_emoji) - message.reset_mock() - @patch(f"{MODULE_PATH}.DuckPond.send_webhook", new_callable=helpers.AsyncMock) @patch(f"{MODULE_PATH}.send_attachments", new_callable=helpers.AsyncMock) @helpers.async_test - async def test_relay_message_to_duck_pond_handles_send_attachments_exceptions(self, send_attachments, send_webhook): - """The `relay_message_to_duck_pond` method should handle exceptions when calling `send_attachment`.""" + async def test_relay_message_handles_irretrievable_attachment_exceptions(self, send_attachments): + """The `relay_message` method should handle irretrievable attachments.""" message = helpers.MockMessage(clean_content="message", attachments=["attachment"]) side_effects = (discord.errors.Forbidden(MagicMock(), ""), discord.errors.NotFound(MagicMock(), "")) self.cog.webhook = helpers.MockAsyncWebhook() log = logging.getLogger("bot.cogs.duck_pond") - # Subtests for the first `except` block for side_effect in side_effects: send_attachments.side_effect = side_effect - with self.subTest(side_effect=type(side_effect).__name__): - with self.assertNotLogs(logger=log, level=logging.ERROR): - await self.cog.relay_message_to_duck_pond(message) + with patch(f"{MODULE_PATH}.DuckPond.send_webhook", new_callable=helpers.AsyncMock) as send_webhook: + with self.subTest(side_effect=type(side_effect).__name__): + with self.assertNotLogs(logger=log, level=logging.ERROR): + await self.cog.relay_message(message) + + self.assertEqual(send_webhook.call_count, 2) + + @patch(f"{MODULE_PATH}.DuckPond.send_webhook", new_callable=helpers.AsyncMock) + @patch(f"{MODULE_PATH}.send_attachments", new_callable=helpers.AsyncMock) + @helpers.async_test + async def test_relay_message_handles_attachment_http_error(self, send_attachments, send_webhook): + """The `relay_message` method should handle irretrievable attachments.""" + message = helpers.MockMessage(clean_content="message", attachments=["attachment"]) - self.assertEqual(send_webhook.call_count, 2) - send_webhook.reset_mock() + self.cog.webhook = helpers.MockAsyncWebhook() + log = logging.getLogger("bot.cogs.duck_pond") - # Subtests for the second `except` block side_effect = discord.HTTPException(MagicMock(), "") send_attachments.side_effect = side_effect with self.subTest(side_effect=type(side_effect).__name__): with self.assertLogs(logger=log, level=logging.ERROR) as log_watcher: - await self.cog.relay_message_to_duck_pond(message) + await self.cog.relay_message(message) send_webhook.assert_called_once_with( content=message.clean_content, @@ -341,10 +349,75 @@ class DuckPondTests(base.LoggingTestCase): self.assertEqual(len(log_watcher.records), 1) - [record] = log_watcher.records - self.assertEqual(record.message, "Failed to send an attachment to the webhook") + record = log_watcher.records[0] self.assertEqual(record.levelno, logging.ERROR) + def _mock_payload(self, label: str, is_custom_emoji: bool, id_: int, emoji_name: str): + """Creates a mock `on_raw_reaction_add` payload with the specified emoji data.""" + payload = MagicMock(name=label) + payload.emoji.is_custom_emoji.return_value = is_custom_emoji + payload.emoji.id = id_ + payload.emoji.name = emoji_name + return payload + + @helpers.async_test + async def test_payload_has_duckpond_emoji_correctly_detects_relevant_emojis(self): + """The `on_raw_reaction_add` event handler should ignore irrelevant emojis.""" + test_values = ( + # Custom Emojis + ( + self._mock_payload( + label="Custom Duckpond Emoji", + is_custom_emoji=True, + id_=constants.DuckPond.custom_emojis[0], + emoji_name="" + ), + True + ), + ( + self._mock_payload( + label="Custom Non-Duckpond Emoji", + is_custom_emoji=True, + id_=123, + emoji_name="" + ), + False + ), + # Unicode Emojis + ( + self._mock_payload( + label="Unicode Duck Emoji", + is_custom_emoji=False, + id_=1, + emoji_name=self.unicode_duck_emoji + ), + True + ), + ( + self._mock_payload( + label="Unicode Non-Duck Emoji", + is_custom_emoji=False, + id_=1, + emoji_name=self.thumbs_up_emoji + ), + False + ), + ) + + for payload, expected_return in test_values: + actual_return = self.cog._payload_has_duckpond_emoji(payload) + with self.subTest(case=payload._mock_name, expected_return=expected_return, actual_return=actual_return): + self.assertEqual(expected_return, actual_return) + + @patch(f"{MODULE_PATH}.discord.utils.get") + @patch(f"{MODULE_PATH}.DuckPond._payload_has_duckpond_emoji", new=MagicMock(return_value=False)) + def test_on_raw_reaction_add_returns_early_with_payload_without_duck_emoji(self, utils_get): + """The `on_raw_reaction_add` method should return early if the payload does not contain a duck emoji.""" + self.assertIsNone(asyncio.run(self.cog.on_raw_reaction_add(payload=MagicMock()))) + + # Ensure we've returned before making an unnecessary API call in the lines of code after the emoji check + utils_get.assert_not_called() + def _raw_reaction_mocks(self, channel_id, message_id, user_id): """Sets up mocks for tests of the `on_raw_reaction_add` event listener.""" channel = helpers.MockTextChannel(id=channel_id) @@ -362,22 +435,6 @@ class DuckPondTests(base.LoggingTestCase): return channel, message, member, payload @helpers.async_test - async def test_on_raw_reaction_add_returns_for_non_relevant_emojis(self): - """The `on_raw_reaction_add` event handler should ignore irrelevant emojis.""" - payload_custom_emoji = MagicMock(label="Non-Duck Custom Emoji") - payload_custom_emoji.emoji.is_custom_emoji.return_value = True - payload_custom_emoji.emoji.id = 12345 - - payload_unicode_emoji = MagicMock(label="Non-Duck Unicode Emoji") - payload_unicode_emoji.emoji.is_custom_emoji.return_value = False - payload_unicode_emoji.emoji.name = self.thumbs_up_emoji - - for payload in (payload_custom_emoji, payload_unicode_emoji): - with self.subTest(case=payload.label), patch(f"{MODULE_PATH}.discord.utils.get") as discord_utils_get: - self.assertIsNone(await self.cog.on_raw_reaction_add(payload)) - discord_utils_get.assert_not_called() - - @helpers.async_test async def test_on_raw_reaction_add_returns_for_bot_and_non_staff_members(self): """The `on_raw_reaction_add` event handler should return for bot users or non-staff members.""" channel_id = 1234 @@ -428,10 +485,8 @@ class DuckPondTests(base.LoggingTestCase): # Assert that we've made it past `self.is_staff` is_staff.assert_called_once() - @patch(f"{MODULE_PATH}.DuckPond.relay_message_to_duck_pond", new_callable=helpers.AsyncMock) - @patch(f"{MODULE_PATH}.DuckPond.count_ducks", new_callable=helpers.AsyncMock) @helpers.async_test - async def test_on_raw_reaction_add_does_not_relay_below_duck_threshold(self, count_ducks, message_relay): + async def test_on_raw_reaction_add_does_not_relay_below_duck_threshold(self): """The `on_raw_reaction_add` listener should not relay messages or attachments below the duck threshold.""" test_cases = ( (constants.DuckPond.threshold - 1, False), @@ -444,21 +499,21 @@ class DuckPondTests(base.LoggingTestCase): payload.emoji = self.duck_pond_emoji for duck_count, should_relay in test_cases: - count_ducks.return_value = duck_count - with self.subTest(duck_count=duck_count, should_relay=should_relay): - await self.cog.on_raw_reaction_add(payload) + with patch(f"{MODULE_PATH}.DuckPond.relay_message", new_callable=helpers.AsyncMock) as relay_message: + with patch(f"{MODULE_PATH}.DuckPond.count_ducks", new_callable=helpers.AsyncMock) as count_ducks: + count_ducks.return_value = duck_count + with self.subTest(duck_count=duck_count, should_relay=should_relay): + await self.cog.on_raw_reaction_add(payload) - # Confirm that we've made it past counting - count_ducks.assert_called_once() - count_ducks.reset_mock() + # Confirm that we've made it past counting + count_ducks.assert_called_once() - # Did we relay a message? - has_relayed = message_relay.called - self.assertEqual(has_relayed, should_relay) + # Did we relay a message? + has_relayed = relay_message.called + self.assertEqual(has_relayed, should_relay) - if should_relay: - message_relay.assert_called_once_with(message) - message_relay.reset_mock() + if should_relay: + relay_message.assert_called_once_with(message) @helpers.async_test async def test_on_raw_reaction_remove_prevents_removal_of_green_checkmark_depending_on_the_duck_count(self): @@ -479,10 +534,10 @@ class DuckPondTests(base.LoggingTestCase): (constants.DuckPond.threshold, True), (constants.DuckPond.threshold + 1, True), ) - for duck_count, should_readd_checkmark in test_cases: + for duck_count, should_re_add_checkmark in test_cases: with patch(f"{MODULE_PATH}.DuckPond.count_ducks", new_callable=helpers.AsyncMock) as count_ducks: count_ducks.return_value = duck_count - with self.subTest(duck_count=duck_count, should_readd_checkmark=should_readd_checkmark): + with self.subTest(duck_count=duck_count, should_re_add_checkmark=should_re_add_checkmark): await self.cog.on_raw_reaction_remove(payload) # Check if we fetched the message @@ -491,16 +546,15 @@ class DuckPondTests(base.LoggingTestCase): # Check if we actually counted the number of ducks count_ducks.assert_called_once_with(message) - has_readded_checkmark = message.add_reaction.called - self.assertEqual(should_readd_checkmark, has_readded_checkmark) + has_re_added_checkmark = message.add_reaction.called + self.assertEqual(should_re_add_checkmark, has_re_added_checkmark) - if should_readd_checkmark: + if should_re_add_checkmark: message.add_reaction.assert_called_once_with(self.checkmark_emoji) message.add_reaction.reset_mock() # reset mocks channel.fetch_message.reset_mock() - count_ducks.reset_mock() message.reset_mock() def test_on_raw_reaction_remove_ignores_removal_of_non_checkmark_reactions(self): @@ -530,7 +584,9 @@ class DuckPondSetupTests(unittest.TestCase): with self.assertLogs(logger=log, level=logging.INFO) as log_watcher: duck_pond.setup(bot) - line = log_watcher.output[0] + + self.assertEqual(len(log_watcher.records), 1) + record = log_watcher.records[0] + self.assertEqual(record.levelno, logging.INFO) bot.add_cog.assert_called_once() - self.assertIn("Cog loaded: DuckPond", line) |