aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/bot/cogs/test_duck_pond.py649
1 files changed, 490 insertions, 159 deletions
diff --git a/tests/bot/cogs/test_duck_pond.py b/tests/bot/cogs/test_duck_pond.py
index 088d8ac79..ceefc286f 100644
--- a/tests/bot/cogs/test_duck_pond.py
+++ b/tests/bot/cogs/test_duck_pond.py
@@ -1,193 +1,524 @@
import asyncio
import logging
+import typing
import unittest
-from unittest.mock import MagicMock
+from unittest.mock import MagicMock, patch
+
+import discord
from bot import constants
from bot.cogs import duck_pond
-from tests.helpers import MockBot, MockEmoji, MockMember, MockMessage, MockReaction, MockRole, MockTextChannel
+from tests import base
+from tests import helpers
+
+MODULE_PATH = "bot.cogs.duck_pond"
+
+
+class DuckPondTests(base.LoggingTestCase):
+ """Tests for DuckPond functionality."""
+
+ @classmethod
+ def setUpClass(cls):
+ """Sets up the objects that only have to be initialized once."""
+ cls.nonstaff_member = helpers.MockMember(name="Non-staffer")
+ cls.staff_role = helpers.MockRole(name="Staff role", id=constants.STAFF_ROLES[0])
+ cls.staff_member = helpers.MockMember(name="staffer", roles=[cls.staff_role])
-class DuckPondTest(unittest.TestCase):
- """Tests the `DuckPond` cog."""
+ cls.checkmark_emoji = "\N{White Heavy Check Mark}"
+ cls.thumbs_up_emoji = "\N{Thumbs Up Sign}"
+ cls.unicode_duck_emoji = "\N{Duck}"
+ cls.duck_pond_emoji = helpers.MockPartialEmoji(id=constants.DuckPond.custom_emojis[0])
+ cls.non_duck_custom_emoji = helpers.MockPartialEmoji(id=123)
def setUp(self):
- """Adds the cog, a bot, and the mocks we'll need for our tests."""
- self.bot = MockBot()
+ """Sets up the objects that need to be refreshed before each test."""
+ self.bot = helpers.MockBot(user=helpers.MockMember(id=46692))
self.cog = duck_pond.DuckPond(bot=self.bot)
- # Set up some constants
- self.CHANNEL_ID = 555
- self.MESSAGE_ID = 666
- self.BOT_ID = 777
- self.CONTRIB_ID = 888
- self.ADMIN_ID = 999
-
- # Override the constants we'll be needing
- constants.STAFF_ROLES = (123,)
- constants.DuckPond.custom_emojis = (789,)
- constants.DuckPond.threshold = 1
-
- # Set up some roles
- self.admin_role = MockRole(name="Admins", role_id=123)
- self.contrib_role = MockRole(name="Contributor", role_id=456)
-
- # Set up some users
- self.admin_member_1 = MockMember(roles=(self.admin_role,), id=self.ADMIN_ID)
- self.admin_member_2 = MockMember(roles=(self.admin_role,), id=911)
- self.contrib_member = MockMember(roles=(self.contrib_role,), id=self.CONTRIB_ID)
- self.bot_member = MockMember(roles=(self.contrib_role,), id=self.BOT_ID, bot=True)
- self.no_role_member = MockMember()
-
- # Set up emojis
- self.checkmark_emoji = "✅"
- self.thumbs_up_emoji = "👍"
- self.unicode_duck_emoji = "🦆"
- self.yellow_ducky_emoji = MockEmoji(id=789)
-
- # Set up reactions
- self.checkmark_reaction = MockReaction(
- emoji=self.checkmark_emoji,
- user_list=[self.admin_member_1]
- )
- self.thumbs_up_reaction = MockReaction(
- emoji=self.thumbs_up_emoji,
- user_list=[self.admin_member_1, self.contrib_member]
- )
- self.yellow_ducky_reaction = MockReaction(
- emoji=self.yellow_ducky_emoji,
- user_list=[self.admin_member_1, self.contrib_member]
- )
- self.unicode_duck_reaction_1 = MockReaction(
- emoji=self.unicode_duck_emoji,
- user_list=[self.admin_member_1]
+ def test_duck_pond_correctly_initializes(self):
+ """`__init__ should set `bot` and `webhook_id` attributes and schedule `fetch_webhook`."""
+ bot = helpers.MockBot()
+ cog = MagicMock()
+
+ duck_pond.DuckPond.__init__(cog, bot)
+
+ self.assertEqual(cog.bot, bot)
+ self.assertEqual(cog.webhook_id, constants.Webhooks.duck_pond)
+ bot.loop.create_loop.called_once_with(cog.fetch_webhook())
+
+ def test_fetch_webhook_succeeds_without_connectivity_issues(self):
+ """The `fetch_webhook` method waits until `READY` event and sets the `webhook` attribute."""
+ self.bot.fetch_webhook.return_value = "dummy webhook"
+ self.cog.webhook_id = 1
+
+ asyncio.run(self.cog.fetch_webhook())
+
+ self.bot.wait_until_ready.assert_called_once()
+ self.bot.fetch_webhook.assert_called_once_with(1)
+ self.assertEqual(self.cog.webhook, "dummy webhook")
+
+ def test_fetch_webhook_logs_when_unable_to_fetch_webhook(self):
+ """The `fetch_webhook` method should log an exception when it fails to fetch the webhook."""
+ self.bot.fetch_webhook.side_effect = discord.HTTPException(response=MagicMock(), message="Not found.")
+ self.cog.webhook_id = 1
+
+ log = logging.getLogger('bot.cogs.duck_pond')
+ with self.assertLogs(logger=log, level=logging.ERROR) as log_watcher:
+ asyncio.run(self.cog.fetch_webhook())
+
+ self.bot.wait_until_ready.assert_called_once()
+ self.bot.fetch_webhook.assert_called_once_with(1)
+
+ 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}`")
+ self.assertEqual(record.levelno, logging.ERROR)
+
+ def test_is_staff_returns_correct_values_based_on_instance_passed(self):
+ """The `is_staff` method should return correct values based on the instance passed."""
+ test_cases = (
+ (helpers.MockUser(name="User instance"), False),
+ (helpers.MockMember(name="Member instance without staff role"), False),
+ (helpers.MockMember(name="Member instance with staff role", roles=[self.staff_role]), True)
)
- self.unicode_duck_reaction_2 = MockReaction(
- emoji=self.unicode_duck_emoji,
- user_list=[self.admin_member_2]
+
+ for user, expected_return in test_cases:
+ actual_return = self.cog.is_staff(user)
+ with self.subTest(user_type=user.name, expected_return=expected_return, actual_return=actual_return):
+ self.assertEqual(expected_return, actual_return)
+
+ @helpers.async_test
+ async def test_has_green_checkmark_correctly_detects_presence_of_green_checkmark_emoji(self):
+ """The `has_green_checkmark` method should only return `True` if one is present."""
+ test_cases = (
+ (
+ "No reactions", helpers.MockMessage(), False
+ ),
+ (
+ "No green check mark reactions",
+ helpers.MockMessage(reactions=[
+ helpers.MockReaction(emoji=self.unicode_duck_emoji),
+ helpers.MockReaction(emoji=self.thumbs_up_emoji)
+ ]),
+ False
+ ),
+ (
+ "Green check mark reaction, but not from our bot",
+ helpers.MockMessage(reactions=[
+ helpers.MockReaction(emoji=self.unicode_duck_emoji),
+ helpers.MockReaction(emoji=self.checkmark_emoji, users=[self.staff_member])
+ ]),
+ False
+ ),
+ (
+ "Green check mark reaction, with one from the bot",
+ helpers.MockMessage(reactions=[
+ helpers.MockReaction(emoji=self.unicode_duck_emoji),
+ helpers.MockReaction(emoji=self.checkmark_emoji, users=[self.staff_member, self.bot.user])
+ ]),
+ True
+ )
)
- self.bot_reaction = MockReaction(
- emoji=self.yellow_ducky_emoji,
- user_list=[self.bot_member]
+
+ for description, message, expected_return in test_cases:
+ actual_return = await self.cog.has_green_checkmark(message)
+ with self.subTest(
+ test_case=description,
+ expected_return=expected_return,
+ actual_return=actual_return
+ ):
+ self.assertEqual(expected_return, actual_return)
+
+ def test_send_webhook_correctly_passes_on_arguments(self):
+ """The `send_webhook` method should pass the arguments to the webhook correctly."""
+ self.cog.webhook = helpers.MockAsyncWebhook()
+
+ content = "fake content"
+ username = "fake username"
+ avatar_url = "fake avatar_url"
+ embed = "fake embed"
+
+ asyncio.run(self.cog.send_webhook(content, username, avatar_url, embed))
+
+ self.cog.webhook.send.assert_called_once_with(
+ content=content,
+ username=username,
+ avatar_url=avatar_url,
+ embed=embed
)
- self.contrib_reaction = MockReaction(
- emoji=self.yellow_ducky_emoji,
- user_list=[self.contrib_member]
+
+ def test_send_webhook_logs_when_sending_message_fails(self):
+ """The `send_webhook` method should catch a `discord.HTTPException` and log accordingly."""
+ self.cog.webhook = helpers.MockAsyncWebhook()
+ self.cog.webhook.send.side_effect = discord.HTTPException(response=MagicMock(), message="Something failed.")
+
+ log = logging.getLogger('bot.cogs.duck_pond')
+ with self.assertLogs(logger=log, level=logging.ERROR) as log_watcher:
+ asyncio.run(self.cog.send_webhook())
+
+ 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")
+ self.assertEqual(record.levelno, logging.ERROR)
+
+ def _get_reaction(
+ self,
+ emoji: typing.Union[str, helpers.MockEmoji],
+ staff: int = 0,
+ nonstaff: int = 0
+ ) -> helpers.MockReaction:
+ staffers = [helpers.MockMember(roles=[self.staff_role]) for _ in range(staff)]
+ nonstaffers = [helpers.MockMember() for _ in range(nonstaff)]
+ return helpers.MockReaction(emoji=emoji, users=staffers + nonstaffers)
+
+ @helpers.async_test
+ async def test_count_ducks_correctly_counts_the_number_of_eligible_duck_emojis(self):
+ """The `count_ducks` method should return the number of unique staffers who gave a duck."""
+ test_cases = (
+ # Simple test cases
+ # A message without reactions should return 0
+ (
+ "No reactions",
+ helpers.MockMessage(),
+ 0
+ ),
+ # A message with a non-duck reaction from a non-staffer should return 0
+ (
+ "Non-duck reaction from non-staffer",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=self.thumbs_up_emoji, nonstaff=1)]),
+ 0
+ ),
+ # A message with a non-duck reaction from a staffer should return 0
+ (
+ "Non-duck reaction from staffer",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=self.non_duck_custom_emoji, staff=1)]),
+ 0
+ ),
+ # A message with a non-duck reaction from a non-staffer and staffer should return 0
+ (
+ "Non-duck reaction from staffer + non-staffer",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=self.thumbs_up_emoji, staff=1, nonstaff=1)]),
+ 0
+ ),
+ # A message with a unicode duck reaction from a non-staffer should return 0
+ (
+ "Unicode Duck Reaction from non-staffer",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=self.unicode_duck_emoji, nonstaff=1)]),
+ 0
+ ),
+ # A message with a unicode duck reaction from a staffer should return 1
+ (
+ "Unicode Duck Reaction from staffer",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=self.unicode_duck_emoji, staff=1)]),
+ 1
+ ),
+ # A message with a unicode duck reaction from a non-staffer and staffer should return 1
+ (
+ "Unicode Duck Reaction from staffer + non-staffer",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=self.unicode_duck_emoji, staff=1, nonstaff=1)]),
+ 1
+ ),
+ # A message with a duckpond duck reaction from a non-staffer should return 0
+ (
+ "Duckpond Duck Reaction from non-staffer",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=self.duck_pond_emoji, nonstaff=1)]),
+ 0
+ ),
+ # A message with a duckpond duck reaction from a staffer should return 1
+ (
+ "Duckpond Duck Reaction from staffer",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=self.duck_pond_emoji, staff=1)]),
+ 1
+ ),
+ # A message with a duckpond duck reaction from a non-staffer and staffer should return 1
+ (
+ "Duckpond Duck Reaction from staffer + non-staffer",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=self.duck_pond_emoji, staff=1, nonstaff=1)]),
+ 1
+ ),
+
+ # Complex test cases
+ # A message with duckpond duck reactions from 3 staffers and 2 non-staffers returns 3
+ (
+ "Duckpond Duck Reaction from 3 staffers + 2 non-staffers",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=self.duck_pond_emoji, staff=3, nonstaff=2)]),
+ 3
+ ),
+ # 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]),
+ ]),
+ 1
+ ),
+ # A non-string emoji does not count (to test the `isinstance(reaction.emoji, str)` elif)
+ (
+ "Reaction with non-Emoji/str emoij from 3 staffers + 2 non-staffers",
+ helpers.MockMessage(reactions=[self._get_reaction(emoji=100, staff=3, nonstaff=2)]),
+ 0
+ ),
+ # 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),
+ ]),
+ 3+4
+ ),
)
- # Set up a messages
- self.checkmark_message = MockMessage(reactions=(self.checkmark_reaction,))
- self.thumbs_up_message = MockMessage(reactions=(self.thumbs_up_reaction,))
- self.yellow_ducky_message = MockMessage(reactions=(self.yellow_ducky_reaction,))
- self.unicode_duck_message = MockMessage(reactions=(self.unicode_duck_reaction_1,))
- self.double_unicode_duck_message = MockMessage(
- reactions=(self.unicode_duck_reaction_1, self.unicode_duck_reaction_2)
+ for description, message, expected_count in test_cases:
+ actual_count = await self.cog.count_ducks(message)
+ with self.subTest(test_case=description, expected_count=expected_count, actual_count=actual_count):
+ 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."""
+ send_webhook_path = f"{MODULE_PATH}.DuckPond.send_webhook"
+ send_attachments_path = f"{MODULE_PATH}.send_attachments"
+
+ self.cog.webhook = helpers.MockAsyncWebhook()
+
+ test_values = (
+ (helpers.MockMessage(clean_content="", attachments=[]), False, False),
+ (helpers.MockMessage(clean_content="message", attachments=[]), True, False),
+ (helpers.MockMessage(clean_content="", attachments=["attachment"]), False, True),
+ (helpers.MockMessage(clean_content="message", attachments=["attachment"]), True, True),
)
- self.double_mixed_duck_message = MockMessage(
- reactions=(self.unicode_duck_reaction_1, self.yellow_ducky_reaction)
+
+ for message, expect_webhook_call, expect_attachment_call in test_values:
+ 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)
+
+ 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`."""
+
+ 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)
+
+ self.assertEqual(send_webhook.call_count, 2)
+ send_webhook.reset_mock()
+
+ # 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)
+
+ send_webhook.assert_called_once_with(
+ content=message.clean_content,
+ username=message.author.display_name,
+ avatar_url=message.author.avatar_url
+ )
+
+ self.assertEqual(len(log_watcher.records), 1)
+
+ [record] = log_watcher.records
+ self.assertEqual(record.message, "Failed to send an attachment to the webhook")
+ self.assertEqual(record.levelno, logging.ERROR)
+
+ 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)
+ self.bot.get_all_channels.return_value = (channel,)
+
+ message = helpers.MockMessage(id=message_id)
+
+ channel.fetch_message.return_value = message
+
+ member = helpers.MockMember(id=user_id, roles=[self.staff_role])
+ message.guild.members = (member,)
+
+ payload = MagicMock(channel_id=channel_id, message_id=message_id, user_id=user_id)
+
+ 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
+ message_id = 2345
+ user_id = 3456
+
+ channel, message, _, payload = self._raw_reaction_mocks(channel_id, message_id, user_id)
+
+ test_cases = (
+ ("non-staff member", helpers.MockMember(id=user_id)),
+ ("bot staff member", helpers.MockMember(id=user_id, roles=[self.staff_role], bot=True)),
)
- self.bot_message = MockMessage(reactions=(self.bot_reaction,))
- self.contrib_message = MockMessage(reactions=(self.contrib_reaction,))
- self.no_reaction_message = MockMessage()
-
- # Set up some channels
- self.text_channel = MockTextChannel(id=self.CHANNEL_ID)
-
- @staticmethod
- def _mock_send_webhook(content, username, avatar_url, embed):
- """Mock for the send_webhook method in DuckPond"""
-
- def test_is_staff_correctly_identifies_staff(self):
- """Test that is_staff correctly identifies a staff member."""
- with self.subTest():
- self.assertTrue(self.cog.is_staff(self.admin_member_1))
- self.assertFalse(self.cog.is_staff(self.contrib_member))
- self.assertFalse(self.cog.is_staff(self.no_role_member))
-
- def test_has_green_checkmark_correctly_identifies_messages(self):
- """Test that has_green_checkmark recognizes messages with checkmarks."""
- with self.subTest():
- self.assertTrue(self.cog.has_green_checkmark(self.checkmark_message))
- self.assertFalse(self.cog.has_green_checkmark(self.thumbs_up_message))
- self.assertFalse(self.cog.has_green_checkmark(self.no_reaction_message))
-
- def test_count_custom_duck_emojis(self):
- """Test that count_ducks counts custom ducks correctly."""
- count_no_ducks = self.cog.count_ducks(self.thumbs_up_message)
- count_one_duck = self.cog.count_ducks(self.yellow_ducky_message)
- with self.subTest():
- self.assertEqual(asyncio.run(count_no_ducks), 0)
- self.assertEqual(asyncio.run(count_one_duck), 1)
-
- def test_count_unicode_duck_emojis(self):
- """Test that count_ducks counts unicode ducks correctly."""
- count_one_duck = self.cog.count_ducks(self.unicode_duck_message)
- count_two_ducks = self.cog.count_ducks(self.double_unicode_duck_message)
-
- with self.subTest():
- self.assertEqual(asyncio.run(count_one_duck), 1)
- self.assertEqual(asyncio.run(count_two_ducks), 2)
-
- def test_count_mixed_duck_emojis(self):
- """Test that count_ducks counts mixed ducks correctly."""
- count_two_ducks = self.cog.count_ducks(self.double_mixed_duck_message)
-
- with self.subTest():
- self.assertEqual(asyncio.run(count_two_ducks), 2)
-
- def test_raw_reaction_add_rejects_bot(self):
- """Test that send_webhook is not called if the user is a bot."""
- self.text_channel.fetch_message.return_value = self.bot_message
- self.bot.get_all_channels.return_value = (self.text_channel,)
-
- payload = MagicMock( # RawReactionActionEvent
- channel_id=self.CHANNEL_ID,
- message_id=self.MESSAGE_ID,
- user_id=self.BOT_ID,
+ payload.emoji = self.duck_pond_emoji
+
+ for description, member in test_cases:
+ message.guild.members = (member, )
+ with self.subTest(test_case=description), patch(f"{MODULE_PATH}.DuckPond.has_green_checkmark") as checkmark:
+ checkmark.side_effect = AssertionError(
+ "Expected method to return before calling `self.has_green_checkmark`."
+ )
+ self.assertIsNone(await self.cog.on_raw_reaction_add(payload))
+
+ # Check that we did make it past the payload checks
+ channel.fetch_message.assert_called_once()
+ channel.fetch_message.reset_mock()
+
+ @patch(f"{MODULE_PATH}.DuckPond.is_staff")
+ @patch(f"{MODULE_PATH}.DuckPond.count_ducks", new_callable=helpers.AsyncMock)
+ def test_on_raw_reaction_add_returns_on_message_with_green_checkmark_placed_by_bot(self, count_ducks, is_staff):
+ """The `on_raw_reaction_add` event should return when the message has a green check mark placed by the bot."""
+ channel_id = 31415926535
+ message_id = 27182818284
+ user_id = 16180339887
+
+ channel, message, member, payload = self._raw_reaction_mocks(channel_id, message_id, user_id)
+
+ payload.emoji = helpers.MockPartialEmoji(name=self.unicode_duck_emoji)
+ payload.emoji.is_custom_emoji.return_value = False
+
+ message.reactions = [helpers.MockReaction(emoji=self.checkmark_emoji, users=[self.bot.user])]
+
+ is_staff.return_value = True
+ count_ducks.side_effect = AssertionError("Expected method to return before calling `self.count_ducks`")
+
+ self.assertIsNone(asyncio.run(self.cog.on_raw_reaction_add(payload)))
+
+ # 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):
+ """The `on_raw_reaction_add` listener should not relay messages or attachments below the duck threshold."""
+ test_cases = (
+ (constants.DuckPond.threshold-1, False),
+ (constants.DuckPond.threshold, True),
+ (constants.DuckPond.threshold+1, True),
)
- with self.subTest():
- asyncio.run(self.cog.on_raw_reaction_add(payload))
- self.bot.cog.send_webhook.assert_not_called()
+ channel, message, member, payload = self._raw_reaction_mocks(channel_id=3, message_id=4, user_id=5)
+
+ 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)
+
+ # Confirm that we've made it past counting
+ count_ducks.assert_called_once()
+ count_ducks.reset_mock()
+
+ # Did we relay a message?
+ has_relayed = message_relay.called
+ self.assertEqual(has_relayed, should_relay)
+
+ if should_relay:
+ message_relay.assert_called_once_with(message)
+ message_relay.reset_mock()
- def test_raw_reaction_add_rejects_non_staff(self):
- """Test that send_webhook is not called if the user is not a member of staff."""
- self.text_channel.fetch_message.return_value = self.contrib_message
- self.bot.get_all_channels.return_value = (self.text_channel,)
+ @helpers.async_test
+ async def test_on_raw_reaction_remove_prevents_removal_of_green_checkmark_depending_on_the_duck_count(self):
+ """The `on_raw_reaction_remove` listener prevents removal of the check mark on messages with enough ducks."""
+ checkmark = helpers.MockPartialEmoji(name=self.checkmark_emoji)
- payload = MagicMock( # RawReactionActionEvent
- channel_id=self.CHANNEL_ID,
- message_id=self.MESSAGE_ID,
- user_id=self.CONTRIB_ID,
+ message = helpers.MockMessage(id=1234)
+
+ channel = helpers.MockTextChannel(id=98765)
+ channel.fetch_message.return_value = message
+
+ self.bot.get_all_channels.return_value = (channel, )
+
+ payload = MagicMock(channel_id=channel.id, message_id=message.id, emoji=checkmark)
+
+ test_cases = (
+ (constants.DuckPond.threshold - 1, False),
+ (constants.DuckPond.threshold, True),
+ (constants.DuckPond.threshold + 1, True),
)
+ for duck_count, should_readd_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):
+ await self.cog.on_raw_reaction_remove(payload)
+
+ # Check if we fetched the message
+ channel.fetch_message.assert_called_once_with(message.id)
- with self.subTest():
- asyncio.run(self.cog.on_raw_reaction_add(payload))
- self.bot.cog.send_webhook.assert_not_called()
+ # Check if we actually counted the number of ducks
+ count_ducks.assert_called_once_with(message)
- def test_raw_reaction_add_sends_message_on_valid_input(self):
- """Test that send_webhook is called if payload is valid."""
- self.text_channel.fetch_message.return_value = self.unicode_duck_message
- self.bot.get_all_channels.return_value = (self.text_channel,)
+ has_readded_checkmark = message.add_reaction.called
+ self.assertEqual(should_readd_checkmark, has_readded_checkmark)
- payload = MagicMock( # RawReactionActionEvent
- channel_id=self.CHANNEL_ID,
- message_id=self.MESSAGE_ID,
- user_id=self.ADMIN_ID,
+ if should_readd_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):
+ """The `on_raw_reaction_remove` listener should ignore the removal of non-check mark emojis."""
+ channel = helpers.MockTextChannel(id=98765)
+
+ channel.fetch_message.side_effect = AssertionError(
+ "Expected method to return before calling `channel.fetch_message`"
)
- with self.subTest():
- asyncio.run(self.cog.on_raw_reaction_add(payload))
- self.bot.cog.send_webhook.assert_called_once()
+ self.bot.get_all_channels.return_value = (channel, )
+
+ payload = MagicMock(emoji=helpers.MockPartialEmoji(name=self.thumbs_up_emoji), channel_id=channel.id)
- def test_raw_reaction_remove_rejects_non_checkmarks(self):
- """A string decoding to numeric characters is a valid user ID."""
- pass
+ self.assertIsNone(asyncio.run(self.cog.on_raw_reaction_remove(payload)))
- def test_raw_reaction_remove_prevents_checkmark_removal(self):
- """A string decoding to numeric characters is a valid user ID."""
- pass
+ channel.fetch_message.assert_not_called()
class DuckPondSetupTests(unittest.TestCase):
@@ -195,7 +526,7 @@ class DuckPondSetupTests(unittest.TestCase):
def test_setup(self):
"""Setup of the cog should log a message at `INFO` level."""
- bot = MockBot()
+ bot = helpers.MockBot()
log = logging.getLogger('bot.cogs.duck_pond')
with self.assertLogs(logger=log, level=logging.INFO) as log_watcher: