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" 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), ) 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(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)
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, 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, users=[self.bot.user]), 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, users=[self.bot.user]), helpers.MockReaction(emoji=self.checkmark_emoji, users=[self.staff_member, self.bot.user]) ]), True)) 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)
async def test_sync_confirmation_context_redirect(self): """If ctx is given, a new message should be sent and author should be ctx's author.""" mock_member = helpers.MockMember() subtests = ( (None, self.bot.user, None), (helpers.MockContext(author=mock_member), mock_member, helpers.MockMessage()), ) for ctx, author, message in subtests: with self.subTest(ctx=ctx, author=author, message=message): if ctx is not None: ctx.send.return_value = message # Make sure `_get_diff` returns a MagicMock, not an AsyncMock self.syncer._get_diff.return_value = mock.MagicMock() self.syncer._get_confirmation_result = mock.AsyncMock( return_value=(False, None)) guild = helpers.MockGuild() await self.syncer.sync(guild, ctx) if ctx is not None: ctx.send.assert_called_once() self.syncer._get_confirmation_result.assert_called_once() self.assertEqual( self.syncer._get_confirmation_result.call_args[0][1], author) self.assertEqual( self.syncer._get_confirmation_result.call_args[0][2], message)
def test_bot_latency_correct_context(self, create_embed, constants): """Ping should return correct ping responses dependent on message sent.""" ctx = helpers.MockContext() ctx.message = helpers.MockMessage() ctx.message.created_at = "D" coroutine = self.cog.ping.callback(self.cog, ctx) self.assertFalse(asyncio.run(coroutine))
async def test_confirmation_result_large_diff(self): """Should return True if confirmed and False if _send_prompt fails or aborted.""" author = helpers.MockMember() mock_message = helpers.MockMessage() subtests = ( (True, mock_message, True, "confirmed"), (False, None, False, "_send_prompt failed"), (False, mock_message, False, "aborted"), ) for expected_result, expected_message, confirmed, msg in subtests: # pragma: no cover with self.subTest(msg=msg): self.syncer._send_prompt = mock.AsyncMock( return_value=expected_message) self.syncer._wait_for_confirmation = mock.AsyncMock( return_value=confirmed) coro = self.syncer._get_confirmation_result(4, author) actual_result, actual_message = await coro self.syncer._send_prompt.assert_called_once_with( None) # message defaults to None self.assertIs(actual_result, expected_result) self.assertEqual(actual_message, expected_message) if expected_message: self.syncer._wait_for_confirmation.assert_called_once_with( author, expected_message)
async def test_sync_respects_confirmation_result(self): """The sync should abort if confirmation fails and continue if confirmed.""" mock_message = helpers.MockMessage() subtests = ( (True, mock_message), (False, None), ) for confirmed, message in subtests: with self.subTest(confirmed=confirmed): self.syncer._sync.reset_mock() self.syncer._get_diff.reset_mock() diff = _Diff({1, 2, 3}, {4, 5}, None) self.syncer._get_diff.return_value = diff self.syncer._get_confirmation_result = mock.AsyncMock( return_value=(confirmed, message)) guild = helpers.MockGuild() await self.syncer.sync(guild) self.syncer._get_diff.assert_called_once_with(guild) self.syncer._get_confirmation_result.assert_called_once() if confirmed: self.syncer._sync.assert_called_once_with(diff) else: self.syncer._sync.assert_not_called()
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.cog.webhook = helpers.MockAsyncWebhook() log = logging.getLogger("bot.cogs.duck_pond") 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(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[0] self.assertEqual(record.levelno, logging.ERROR)
async def test_send_prompt_edits_and_returns_message(self): """The given message should be edited to display the prompt and then should be returned.""" msg = helpers.MockMessage() ret_val = await self.syncer._send_prompt(msg) msg.edit.assert_called_once() self.assertIn("content", msg.edit.call_args[1]) self.assertEqual(ret_val, msg)
def test_bot_latency_correct_time(self, create_embed, constants): """Ping should return correct ping responses dependent on message sent.""" ctx = helpers.MockContext() ctx.message = helpers.MockMessage() timestamp = 1587263832 ctx.message.created_at = datetime.fromtimestamp(timestamp) self.assertEqual( information.time_difference_milliseconds(datetime.fromtimestamp(1587263836), ctx.message), 4000)
def test_reaction_check_for_invalid_reactions(self): """Should return False for invalid reaction events.""" valid_emoji = self.syncer._REACTION_EMOJIS[0] subtests = ( ( helpers.MockMember(id=77), *self.get_message_reaction(valid_emoji), helpers.MockMember(id=43, roles=[self.core_dev_role]), "users are not identical", ), ( helpers.MockMember(id=77, bot=True), *self.get_message_reaction(valid_emoji), helpers.MockMember(id=43), "reactor lacks the core-dev role", ), ( helpers.MockMember(id=77, bot=True, roles=[self.core_dev_role]), *self.get_message_reaction(valid_emoji), helpers.MockMember(id=77, bot=True, roles=[self.core_dev_role]), "reactor is a bot", ), ( helpers.MockMember(id=77), helpers.MockMessage(id=95), helpers.MockReaction(emoji=valid_emoji, message=helpers.MockMessage(id=26)), helpers.MockMember(id=77), "messages are not identical", ), ( helpers.MockMember(id=77), *self.get_message_reaction("InVaLiD"), helpers.MockMember(id=77), "emoji is invalid", ), ) for *args, msg in subtests: kwargs = dict(zip(("author", "message", "reaction", "user"), args)) with self.subTest(**kwargs, msg=msg): ret_val = self.syncer._reaction_check(*args) self.assertFalse(ret_val)
async def test_embedjson_command(self): context = helpers.MockContext() await self.cog.embed_json(self.cog, context, message=helpers.MockMessage()) self.assertEqual( context.send.call_args.kwargs["embed"].description[:61], '```json\n<MagicMock name="mock.embeds.__getitem__().to_dict()"', )
async def test_sync_message_edited(self): """The message should be edited if one was sent, even if the sync has an API error.""" subtests = ( (None, None, False), (helpers.MockMessage(), None, True), (helpers.MockMessage(), ResponseCodeError(mock.MagicMock()), True), ) for message, side_effect, should_edit in subtests: with self.subTest(message=message, side_effect=side_effect, should_edit=should_edit): TestSyncer._sync.side_effect = side_effect ctx = helpers.MockContext() ctx.send.return_value = message await TestSyncer.sync(self.guild, ctx) if should_edit: message.edit.assert_called_once() self.assertIn("content", message.edit.call_args[1])
def mock_get_channel(self): """Fixture to return a mock channel and message for when `get_channel` is used.""" self.bot.reset_mock() mock_channel = helpers.MockTextChannel() mock_message = helpers.MockMessage() mock_channel.send.return_value = mock_message self.bot.get_channel.return_value = mock_channel return mock_channel, mock_message
async def test_sync_message_edited(self): """The message should be edited if one was sent, even if the sync has an API error.""" subtests = ( (None, None, False), (helpers.MockMessage(), None, True), (helpers.MockMessage(), ResponseCodeError(mock.MagicMock()), True), ) for message, side_effect, should_edit in subtests: with self.subTest(message=message, side_effect=side_effect, should_edit=should_edit): self.syncer._sync.side_effect = side_effect self.syncer._get_confirmation_result = mock.AsyncMock( return_value=(True, message)) guild = helpers.MockGuild() await self.syncer.sync(guild) if should_edit: message.edit.assert_called_once() self.assertIn("content", message.edit.call_args[1])
async def test_sync_message_sent(self): """If ctx is given, a new message should be sent.""" subtests = ( (None, None), (helpers.MockContext(), helpers.MockMessage()), ) for ctx, message in subtests: with self.subTest(ctx=ctx, message=message): await TestSyncer.sync(self.guild, ctx) if ctx is not None: ctx.send.assert_called_once()
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
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) 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_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_re_add_checkmark=should_re_add_checkmark): await self.cog.on_raw_reaction_remove(payload) # Check if we fetched the message channel.fetch_message.assert_called_once_with(message.id) # Check if we actually counted the number of ducks count_ducks.assert_called_once_with(message) has_re_added_checkmark = message.add_reaction.called self.assertEqual(should_re_add_checkmark, has_re_added_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() message.reset_mock()
def test_mocks_rejects_access_to_attributes_not_part_of_spec(self): """Accessing attributes that are invalid for the objects they mock should fail.""" mocks = ( helpers.MockGuild(), helpers.MockRole(), helpers.MockMember(), helpers.MockBot(), helpers.MockContext(), helpers.MockTextChannel(), helpers.MockMessage(), ) for mock in mocks: with self.subTest(mock=mock): with self.assertRaises(AttributeError): mock.the_cake_is_a_lie
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") for side_effect in side_effects: # pragma: no cover send_attachments.side_effect = side_effect with patch(f"{MODULE_PATH}.DuckPond.send_webhook", new_callable=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)
async def test_wait_for_confirmation(self): """The message should always be edited and only return True if the emoji is a check mark.""" subtests = ( (constants.Emojis.check_mark, True, None), ("InVaLiD", False, None), (None, False, asyncio.TimeoutError), ) for emoji, ret_val, side_effect in subtests: for bot in (True, False): with self.subTest(emoji=emoji, ret_val=ret_val, side_effect=side_effect, bot=bot): # Set up mocks message = helpers.MockMessage() member = helpers.MockMember(bot=bot) self.bot.wait_for.reset_mock() self.bot.wait_for.return_value = (helpers.MockReaction( emoji=emoji), None) self.bot.wait_for.side_effect = side_effect # Call the function actual_return = await self.syncer._wait_for_confirmation( member, message) # Perform assertions self.bot.wait_for.assert_called_once() self.assertIn("reaction_add", self.bot.wait_for.call_args[0]) message.edit.assert_called_once() kwargs = message.edit.call_args[1] self.assertIn("content", kwargs) # Core devs should only be mentioned if the author is a bot. if bot: self.assertIn(self.syncer._CORE_DEV_MENTION, kwargs["content"]) else: self.assertNotIn(self.syncer._CORE_DEV_MENTION, kwargs["content"]) self.assertIs(actual_return, ret_val)
async def test_confirmation_result_small_diff(self): """Should always return True and the given message if the diff size is too small.""" author = helpers.MockMember() expected_message = helpers.MockMessage() for size in (3, 2): # pragma: no cover with self.subTest(size=size): self.syncer._send_prompt = mock.AsyncMock() self.syncer._wait_for_confirmation = mock.AsyncMock() coro = self.syncer._get_confirmation_result( size, author, expected_message) result, actual_message = await coro self.assertTrue(result) self.assertEqual(actual_message, expected_message) self.syncer._send_prompt.assert_not_called() self.syncer._wait_for_confirmation.assert_not_called()
def test_mocks_allows_access_to_attributes_part_of_spec(self): """Accessing attributes that are valid for the objects they mock should succeed.""" mocks = ( (helpers.MockGuild(), 'name'), (helpers.MockRole(), 'hoist'), (helpers.MockMember(), 'display_name'), (helpers.MockBot(), 'user'), (helpers.MockContext(), 'invoked_with'), (helpers.MockTextChannel(), 'last_message'), (helpers.MockMessage(), 'mention_everyone'), ) for mock, valid_attribute in mocks: with self.subTest(mock=mock): try: getattr(mock, valid_attribute) except AttributeError: msg = f"accessing valid attribute `{valid_attribute}` raised an AttributeError" self.fail(msg)
async def test_send_prompt_adds_reactions(self): """The message should have reactions for confirmation added.""" extant_message = helpers.MockMessage() subtests = ( (extant_message, lambda: (None, extant_message)), (None, self.mock_get_channel), (None, self.mock_fetch_channel), ) for message_arg, mock_ in subtests: subtest_msg = "Extant message" if mock_.__name__ == "<lambda>" else mock_.__name__ with self.subTest(msg=subtest_msg): _, mock_message = mock_() await self.syncer._send_prompt(message_arg) calls = [ mock.call(emoji) for emoji in self.syncer._REACTION_EMOJIS ] mock_message.add_reaction.assert_has_calls(calls)
def setUp(self): """Set up a clean environment for each test.""" self.bot = helpers.MockMrFreeze() self.cog = TemperatureConverter(self.bot) self.msg = helpers.MockMessage() self.channel = self.msg.channel self.author = helpers.MockMember() self.author.bot = False self.msg.author = self.author self.ctx = helpers.MockContext() self.ctx.message = self.msg self.ctx.author = self.author self.ctx.channel = self.channel self.bot.get_context.return_value = self.ctx self.files: List[File] = list()
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), ) 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)
def get_message_reaction(emoji): """Fixture to return a mock message an reaction from the given `emoji`.""" message = helpers.MockMessage() reaction = helpers.MockReaction(emoji=emoji, message=message) return message, reaction