From 13866a11f764dec27bc5a600781caa17986e9957 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 16 Nov 2020 01:48:38 +0300 Subject: Adds VoiceChannel Mute Adds an optional channel parameter to silence and unsilence commands, and adds ability to silence voice channels. TODO: New Tests Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 104293d8e..bac933115 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -123,7 +123,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): guild.get_role.side_effect = lambda id_: Mock(id=id_) await self.cog._async_init() - self.assertEqual(self.cog._verified_role.id, Roles.verified) + self.assertEqual(self.cog._verified_msg_role.id, Roles.verified) @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_async_init_got_channels(self): @@ -277,7 +277,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=was_silenced): with self.subTest(was_silenced=was_silenced, message=message, duration=duration): await self.cog.silence.callback(self.cog, ctx, duration) - ctx.send.assert_called_once_with(message) + ctx.channel.send.assert_called_once_with(message) async def test_skipped_already_silenced(self): """Permissions were not set and `False` was returned for an already silenced channel.""" @@ -302,7 +302,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.assertFalse(self.overwrite.send_messages) self.assertFalse(self.overwrite.add_reactions) self.channel.set_permissions.assert_awaited_once_with( - self.cog._verified_role, + self.cog._verified_msg_role, overwrite=self.overwrite ) @@ -372,7 +372,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): args = (300, ctx.channel.id, ctx.invoke.return_value) self.cog.scheduler.schedule_later.assert_called_once_with(*args) - ctx.invoke.assert_called_once_with(self.cog.unsilence) + ctx.invoke.assert_called_once_with(self.cog.unsilence, channel=ctx.channel) async def test_permanent_not_scheduled(self): """A task was not scheduled for a permanent silence.""" @@ -435,7 +435,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): """Channel's `send_message` and `add_reactions` overwrites were restored.""" await self.cog._unsilence(self.channel) self.channel.set_permissions.assert_awaited_once_with( - self.cog._verified_role, + self.cog._verified_msg_role, overwrite=self.overwrite, ) @@ -449,7 +449,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._unsilence(self.channel) self.channel.set_permissions.assert_awaited_once_with( - self.cog._verified_role, + self.cog._verified_msg_role, overwrite=self.overwrite, ) -- cgit v1.2.3 From 81f923ed784cf09e3b7e90e57a9ad0111099619c Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 23 Nov 2020 01:24:15 +0300 Subject: Write AnyChannelConverter Tests Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/test_converters.py | 48 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) (limited to 'tests') diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py index c42111f3f..6bea71977 100644 --- a/tests/bot/test_converters.py +++ b/tests/bot/test_converters.py @@ -7,6 +7,7 @@ from dateutil.relativedelta import relativedelta from discord.ext.commands import BadArgument from bot.converters import ( + AnyChannelConverter, Duration, HushDurationConverter, ISODateTime, @@ -24,6 +25,18 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase): cls.context = MagicMock cls.context.author = 'bob' + cls.context.guild = MagicMock + cls.context.guild.channels = [] + + for i in range(10): + channel = MagicMock() + channel.name = f"test-{i}" + channel.id = str(i) * 18 + + cls.context.guild.channels.append(channel) + if i > 7: + cls.context.guild.channels.append(channel) + cls.fixed_utc_now = datetime.datetime.fromisoformat('2019-01-01T00:00:00') async def test_tag_content_converter_for_valid(self): @@ -312,3 +325,38 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase): with self.subTest(invalid_minutes_string=invalid_minutes_string, exception_message=exception_message): with self.assertRaisesRegex(BadArgument, re.escape(exception_message)): await converter.convert(self.context, invalid_minutes_string) + + async def test_any_channel_converter_for_valid(self): + """AnyChannelConverter returns correct channel from input.""" + test_values = ( + ("test-0", self.context.guild.channels[0]), + ("#test-0", self.context.guild.channels[0]), + (" #tEsT-0 ", self.context.guild.channels[0]), + (f"<#{'0' * 18}>", self.context.guild.channels[0]), + (f"{'0' * 18}", self.context.guild.channels[0]), + ("test-5", self.context.guild.channels[5]), + ("#test-5", self.context.guild.channels[5]), + (f"<#{'5' * 18}>", self.context.guild.channels[5]), + (f"{'5' * 18}", self.context.guild.channels[5]), + ) + + converter = AnyChannelConverter() + for input_string, expected_channel in test_values: + with self.subTest(input_string=input_string, expected_channel=expected_channel): + converted = await converter.convert(self.context, input_string) + self.assertEqual(expected_channel, converted) + + async def test_any_channel_converter_for_invalid(self): + """AnyChannelConverter raises BadArgument for invalid channels.""" + test_values = ( + ("#test-8", "The provided argument returned too many matches (2)."), + ("#test-9", "The provided argument returned too many matches (2)."), + ("#random-name", "#random-name returned no matches."), + ("test-10", "test-10 returned no matches.") + ) + + converter = AnyChannelConverter() + for invalid_input, exception_message in test_values: + with self.subTest(invalid_input=invalid_input, exception_message=exception_message): + with self.assertRaisesRegex(BadArgument, re.escape(exception_message)): + await converter.convert(self.context, invalid_input) -- cgit v1.2.3 From 483114bc29da713870346c0e4b88d008f6281185 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 23 Nov 2020 14:24:27 +0300 Subject: General Silence Class Tests Adds tests for helper functions in the silence cog. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/moderation/silence.py | 27 +++++---- tests/bot/exts/moderation/test_silence.py | 91 ++++++++++++++++++++++++++++++- tests/helpers.py | 25 ++++++++- 3 files changed, 126 insertions(+), 17 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 2d928182a..2aebee9d7 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -104,6 +104,20 @@ class Silence(commands.Cog): self.notifier = SilenceNotifier(self.bot.get_channel(Channels.mod_log)) await self._reschedule() + async def _get_related_text_channel(self, channel: VoiceChannel) -> Optional[TextChannel]: + """Returns the text channel related to a voice channel.""" + # TODO: Figure out a dynamic way of doing this + channels = { + "off-topic": Channels.voice_chat, + "code/help 1": Channels.code_help_voice, + "code/help 2": Channels.code_help_voice, + "admin": Channels.admins_voice, + "staff": Channels.staff_voice + } + for name in channels.keys(): + if name in channel.name.lower(): + return self.bot.get_channel(channels[name]) + async def send_message(self, message: str, source_channel: TextChannel, target_channel: Union[TextChannel, VoiceChannel], alert_target: bool = False, duration: HushDurationConverter = 0) -> None: @@ -116,18 +130,7 @@ class Silence(commands.Cog): voice_chat = None if isinstance(target_channel, VoiceChannel): # Send to relevant channel - # TODO: Figure out a dynamic way of doing this - channels = { - "offtopic": Channels.voice_chat, - "code/help 1": Channels.code_help_voice, - "code/help 2": Channels.code_help_voice, - "admin": Channels.admins_voice, - "staff": Channels.staff_voice - } - for name in channels.keys(): - if name in target_channel.name.lower(): - voice_chat = self.bot.get_channel(channels[name]) - break + voice_chat = await self._get_related_text_channel(target_channel) if alert_target and source_channel != target_channel: if isinstance(target_channel, VoiceChannel): diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index bac933115..577725071 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -7,9 +7,9 @@ from unittest.mock import Mock from async_rediscache import RedisSession from discord import PermissionOverwrite -from bot.constants import Channels, Guild, Roles +from bot.constants import Channels, Emojis, Guild, Roles from bot.exts.moderation import silence -from tests.helpers import MockBot, MockContext, MockTextChannel, autospec +from tests.helpers import MockBot, MockContext, MockTextChannel, MockVoiceChannel, autospec redis_session = None redis_loop = asyncio.get_event_loop() @@ -168,6 +168,93 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): role_check.assert_called_once_with(*(1, 2, 3)) role_check.return_value.predicate.assert_awaited_once_with(ctx) + @mock.patch.object(silence.Silence, "_get_related_text_channel") + async def test_send_message(self, mock_get_related_text_channel): + """Test the send function reports to the correct channels.""" + text_channel_1 = MockTextChannel() + text_channel_2 = MockTextChannel() + + voice_channel = MockVoiceChannel() + voice_channel.name = "General/Offtopic" + voice_channel.mention = f"#{voice_channel.name}" + + mock_get_related_text_channel.return_value = text_channel_2 + + def reset(): + text_channel_1.reset_mock() + text_channel_2.reset_mock() + voice_channel.reset_mock() + + with self.subTest("Basic One Channel Test"): + await self.cog.send_message("Text basic message.", text_channel_1, text_channel_2, False) + text_channel_1.send.assert_called_once_with("Text basic message.") + text_channel_2.send.assert_not_called() + + reset() + with self.subTest("Basic Two Channel Test"): + await self.cog.send_message("Text basic message.", text_channel_1, text_channel_2, True) + text_channel_1.send.assert_called_once_with("Text basic message.") + text_channel_2.send.assert_called_once_with("Text basic message.") + + reset() + with self.subTest("Replacement One Channel Test"): + await self.cog.send_message("The following should be replaced: current", + text_channel_1, text_channel_2, False) + text_channel_1.send.assert_called_once_with(f"The following should be replaced: {text_channel_1.mention}") + text_channel_2.send.assert_not_called() + + reset() + with self.subTest("Replacement Two Channel Test"): + await self.cog.send_message("The following should be replaced: current", + text_channel_1, text_channel_2, True) + text_channel_1.send.assert_called_once_with(f"The following should be replaced: {text_channel_1.mention}") + text_channel_2.send.assert_called_once_with("The following should be replaced: current") + + reset() + with self.subTest("Replace Duration"): + await self.cog.send_message(f"{Emojis.check_mark} The following should be replaced: {{duration}}", + text_channel_1, text_channel_2, False) + text_channel_1.send.assert_called_once_with(f"{Emojis.check_mark} The following should be replaced: 0") + text_channel_2.send.assert_not_called() + + reset() + with self.subTest("Text and Voice"): + await self.cog.send_message("This should show up just here", + text_channel_1, voice_channel, False) + text_channel_1.send.assert_called_once_with("This should show up just here") + + reset() + with self.subTest("Text and Voice"): + await self.cog.send_message("This should show up as current", + text_channel_1, voice_channel, True) + text_channel_1.send.assert_called_once_with(f"This should show up as {voice_channel.mention}") + text_channel_2.send.assert_called_once_with(f"This should show up as {voice_channel.mention}") + + reset() + with self.subTest("Text and Voice Same Invocation"): + await self.cog.send_message("This should show up as current", + text_channel_2, voice_channel, True) + text_channel_2.send.assert_called_once_with(f"This should show up as {voice_channel.mention}") + + async def test_get_related_text_channel(self): + voice_channel = MockVoiceChannel() + + tests = ( + ("Off-Topic/General", Channels.voice_chat), + ("code/help 1", Channels.code_help_voice), + ("Staff", Channels.staff_voice), + ("ADMIN", Channels.admins_voice), + ("not in the channel list", None) + ) + + with mock.patch.object(self.cog.bot, "get_channel", lambda x: x): + for (name, channel_id) in tests: + voice_channel.name = name + voice_channel.id = channel_id + + result_id = await self.cog._get_related_text_channel(voice_channel) + self.assertEqual(result_id, channel_id) + @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) class RescheduleTests(unittest.IsolatedAsyncioTestCase): diff --git a/tests/helpers.py b/tests/helpers.py index 870f66197..5628ca31f 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -16,7 +16,6 @@ from bot.async_stats import AsyncStatsClient from bot.bot import Bot from tests._autospec import autospec # noqa: F401 other modules import it via this module - for logger in logging.Logger.manager.loggerDict.values(): # Set all loggers to CRITICAL by default to prevent screen clutter during testing @@ -320,7 +319,10 @@ channel_data = { } state = unittest.mock.MagicMock() guild = unittest.mock.MagicMock() -channel_instance = discord.TextChannel(state=state, guild=guild, data=channel_data) +text_channel_instance = discord.TextChannel(state=state, guild=guild, data=channel_data) + +channel_data["type"] = "VoiceChannel" +voice_channel_instance = discord.VoiceChannel(state=state, guild=guild, data=channel_data) class MockTextChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): @@ -330,7 +332,24 @@ class MockTextChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): Instances of this class will follow the specifications of `discord.TextChannel` instances. For more information, see the `MockGuild` docstring. """ - spec_set = channel_instance + spec_set = text_channel_instance + + def __init__(self, **kwargs) -> None: + default_kwargs = {'id': next(self.discord_id), 'name': 'channel', 'guild': MockGuild()} + super().__init__(**collections.ChainMap(kwargs, default_kwargs)) + + if 'mention' not in kwargs: + self.mention = f"#{self.name}" + + +class MockVoiceChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): + """ + A MagicMock subclass to mock VoiceChannel objects. + + Instances of this class will follow the specifications of `discord.VoiceChannel` instances. For + more information, see the `MockGuild` docstring. + """ + spec_set = voice_channel_instance def __init__(self, **kwargs) -> None: default_kwargs = {'id': next(self.discord_id), 'name': 'channel', 'guild': MockGuild()} -- cgit v1.2.3 From 00a26b95bc248e3e7b13263df8c6c0d46ebbb2ff Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 23 Nov 2020 23:09:13 +0300 Subject: Adds Silence & Unsilence UnitTests Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 212 +++++++++++++++++++++++++++++- 1 file changed, 211 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 577725071..1d05ee357 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -9,7 +9,7 @@ from discord import PermissionOverwrite from bot.constants import Channels, Emojis, Guild, Roles from bot.exts.moderation import silence -from tests.helpers import MockBot, MockContext, MockTextChannel, MockVoiceChannel, autospec +from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockTextChannel, MockVoiceChannel, autospec redis_session = None redis_loop = asyncio.get_event_loop() @@ -237,6 +237,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): text_channel_2.send.assert_called_once_with(f"This should show up as {voice_channel.mention}") async def test_get_related_text_channel(self): + """Tests the helper function that connects voice to text channels.""" voice_channel = MockVoiceChannel() tests = ( @@ -255,6 +256,72 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): result_id = await self.cog._get_related_text_channel(voice_channel) self.assertEqual(result_id, channel_id) + async def test_force_voice_sync(self): + """Tests the _force_voice_sync helper function.""" + await self.cog._async_init() + + afk_channel = MockVoiceChannel() + channel = MockVoiceChannel(guild=MockGuild(afk_channel=afk_channel)) + + members = [] + for _ in range(10): + members.append(MockMember()) + + channel.members = members + test_cases = ( + (members[0], False, "Muting member."), + (members[0], True, "Kicking voice channel member."), + (None, False, "Muting member."), + (None, True, "Kicking voice channel member."), + ) + + for member, kick, reason in test_cases: + with self.subTest(members=member, kick=kick, reason=reason): + await self.cog._force_voice_sync(channel, member, kick) + + for single_member in channel.members if member is None else [member]: + if kick: + single_member.move_to.assert_called_once_with(None, reason=reason) + else: + self.assertEqual(single_member.move_to.call_count, 2) + single_member.move_to.assert_has_calls([ + mock.call(afk_channel, reason=reason), + mock.call(channel, reason=reason) + ], any_order=False) + + single_member.reset_mock() + + async def test_force_voice_sync_staff(self): + """Tests to ensure _force_voice_sync does not kick staff members.""" + await self.cog._async_init() + member = MockMember(roles=[self.cog._helper_role]) + + await self.cog._force_voice_sync(MockVoiceChannel(), member) + member.move_to.assert_not_called() + + async def test_force_voice_sync_no_channel(self): + """Test to ensure _force_voice_sync can create its own voice channel if one is not available.""" + await self.cog._async_init() + + member = MockMember() + channel = MockVoiceChannel(guild=MockGuild(afk_channel=None)) + + new_channel = MockVoiceChannel(delete=Mock()) + channel.guild.create_voice_channel.return_value = new_channel + + with mock.patch.object(self.cog.scheduler, "schedule_later") as scheduler: + await self.cog._force_voice_sync(channel, member) + + # Check channel creation + overwrites = { + channel.guild.default_role: PermissionOverwrite(speak=False, connect=False, view_channel=False) + } + channel.guild.create_voice_channel.assert_called_once_with("mute-temp", overwrites=overwrites) + + # Check bot queued deletion + new_channel.delete.assert_called_once_with(reason="Deleting temp mute channel.") + scheduler.assert_called_once_with(30, new_channel.id, new_channel.delete()) + @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) class RescheduleTests(unittest.IsolatedAsyncioTestCase): @@ -366,6 +433,31 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog.silence.callback(self.cog, ctx, duration) ctx.channel.send.assert_called_once_with(message) + async def test_sent_to_correct_channel(self): + """Test function sends messages to the correct channels.""" + text_channel = MockTextChannel() + ctx = MockContext() + + test_cases = ( + (None, silence.MSG_SILENCE_SUCCESS.format(duration=10)), + (text_channel, silence.MSG_SILENCE_SUCCESS.format(duration=10)), + (ctx.channel, silence.MSG_SILENCE_SUCCESS.format(duration=10)), + ) + + for target, message in test_cases: + with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=True): + with self.subTest(target_channel=target, message=message): + await self.cog.silence.callback(self.cog, ctx, 10, False, channel=target) + if ctx.channel == target or target is None: + ctx.channel.send.assert_called_once_with(message) + else: + ctx.channel.send.assert_called_once_with(message.replace("current", text_channel.mention)) + target.send.assert_called_once_with(message) + + ctx.channel.send.reset_mock() + if target is not None: + target.send.reset_mock() + async def test_skipped_already_silenced(self): """Permissions were not set and `False` was returned for an already silenced channel.""" subtests = ( @@ -509,6 +601,40 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog.unsilence.callback(self.cog, ctx) ctx.channel.send.assert_called_once_with(message) + async def test_sent_to_correct_channel(self): + """Test function sends messages to the correct channels.""" + unsilenced_overwrite = PermissionOverwrite(send_messages=True, add_reactions=True) + text_channel = MockTextChannel() + ctx = MockContext() + + test_cases = ( + (None, silence.MSG_UNSILENCE_SUCCESS.format(duration=10)), + (text_channel, silence.MSG_UNSILENCE_SUCCESS.format(duration=10)), + (ctx.channel, silence.MSG_UNSILENCE_SUCCESS.format(duration=10)), + ) + + for target, message in test_cases: + with self.subTest(target_channel=target, message=message): + with mock.patch.object(self.cog, "_unsilence", return_value=True): + # Assign Return + if ctx.channel == target or target is None: + ctx.channel.overwrites_for.return_value = unsilenced_overwrite + else: + target.overwrites_for.return_value = unsilenced_overwrite + + await self.cog.unsilence.callback(self.cog, ctx, channel=target) + + # Check Messages + if ctx.channel == target or target is None: + ctx.channel.send.assert_called_once_with(message) + else: + ctx.channel.send.assert_called_once_with(message.replace("current", text_channel.mention)) + target.send.assert_called_once_with(message) + + ctx.channel.send.reset_mock() + if target is not None: + target.send.reset_mock() + async def test_skipped_already_unsilenced(self): """Permissions were not set and `False` was returned for an already unsilenced channel.""" self.cog.scheduler.__contains__.return_value = False @@ -549,6 +675,10 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._unsilence(self.channel) self.cog._mod_alerts_channel.send.assert_awaited_once() + self.cog._mod_alerts_channel.send.reset_mock() + + await self.cog._unsilence(MockVoiceChannel()) + self.cog._mod_alerts_channel.send.assert_awaited_once() async def test_removed_notifier(self): """Channel was removed from `notifier`.""" @@ -587,3 +717,83 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): del new_overwrite_dict['add_reactions'] self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) + + async def test_unsilence_helper_fail(self): + """Tests unsilence_wrapper when `_unsilence` fails.""" + ctx = MockContext() + + text_channel = MockTextChannel() + text_role = self.cog.bot.get_guild(Guild.id).get_role(Roles.verified) + + voice_channel = MockVoiceChannel() + voice_role = self.cog.bot.get_guild(Guild.id).get_role(Roles.voice_verified) + + test_cases = ( + (ctx, text_channel, text_role, True, silence.MSG_UNSILENCE_FAIL), + (ctx, text_channel, text_role, False, silence.MSG_UNSILENCE_MANUAL), + (ctx, voice_channel, voice_role, True, silence.MSG_UNSILENCE_FAIL), + (ctx, voice_channel, voice_role, False, silence.MSG_UNSILENCE_MANUAL), + ) + + class PermClass: + """Class to Mock return permissions""" + def __init__(self, value: bool): + self.val = value + + def __getattr__(self, item): + return self.val + + for context, channel, role, permission, message in test_cases: + with self.subTest(channel=channel, message=message): + with mock.patch.object(channel, "overwrites_for", return_value=PermClass(permission)) as overwrites: + with mock.patch.object(self.cog, "send_message") as send_message: + with mock.patch.object(self.cog, "_unsilence", return_value=False): + await self.cog._unsilence_wrapper(channel, context) + + overwrites.assert_called_once_with(role) + send_message.assert_called_once_with(message, ctx.channel, channel) + + async def test_correct_overwrites(self): + """Tests the overwrites returned by the _unsilence_wrapper are correct for voice and text channels.""" + ctx = MockContext() + + text_channel = MockTextChannel() + text_role = self.cog.bot.get_guild(Guild.id).get_role(Roles.verified) + + voice_channel = MockVoiceChannel() + voice_role = self.cog.bot.get_guild(Guild.id).get_role(Roles.voice_verified) + + async def reset(): + await text_channel.set_permissions(text_role, PermissionOverwrite(send_messages=False, add_reactions=False)) + await voice_channel.set_permissions(voice_role, PermissionOverwrite(speak=False, connect=False)) + + text_channel.reset_mock() + voice_channel.reset_mock() + await reset() + + default_text_overwrites = text_channel.overwrites_for(text_role) + default_voice_overwrites = voice_channel.overwrites_for(voice_role) + + test_cases = ( + (ctx, text_channel, text_role, default_text_overwrites, silence.MSG_UNSILENCE_SUCCESS), + (ctx, voice_channel, voice_role, default_voice_overwrites, silence.MSG_UNSILENCE_SUCCESS), + (ctx, ctx.channel, text_role, ctx.channel.overwrites_for(text_role), silence.MSG_UNSILENCE_SUCCESS), + (None, text_channel, text_role, default_text_overwrites, silence.MSG_UNSILENCE_SUCCESS), + ) + + for context, channel, role, overwrites, message in test_cases: + with self.subTest(ctx=context, channel=channel): + with mock.patch.object(self.cog, "send_message") as send_message: + with mock.patch.object(self.cog, "_force_voice_sync"): + await self.cog._unsilence_wrapper(channel, context) + + if context is None: + send_message.assert_called_once_with(message, channel, channel, True) + else: + send_message.assert_called_once_with(message, context.channel, channel, True) + + channel.set_permissions.assert_called_once_with(role, overwrite=overwrites) + if channel != ctx.channel: + ctx.channel.assert_not_called() + + await reset() -- cgit v1.2.3 From dc202302fa9bfa7ab54d80cc486acdd5b278ad6a Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 23 Nov 2020 23:50:23 +0300 Subject: Finalizes Silence & Unsilence UnitTests Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 49 ++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 17 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 1d05ee357..2392e6e59 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -415,9 +415,13 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): asyncio.run(self.cog._async_init()) # Populate instance attributes. - self.channel = MockTextChannel() - self.overwrite = PermissionOverwrite(stream=True, send_messages=True, add_reactions=False) - self.channel.overwrites_for.return_value = self.overwrite + self.text_channel = MockTextChannel() + self.text_overwrite = PermissionOverwrite(stream=True, send_messages=True, add_reactions=False) + self.text_channel.overwrites_for.return_value = self.text_overwrite + + self.voice_channel = MockVoiceChannel() + self.voice_overwrite = PermissionOverwrite(speak=True) + self.voice_channel.overwrites_for.return_value = self.voice_overwrite async def test_sent_correct_message(self): """Appropriate failure/success message was sent by the command.""" @@ -477,19 +481,19 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_silenced_channel(self): """Channel had `send_message` and `add_reactions` permissions revoked for verified role.""" - self.assertTrue(await self.cog._set_silence_overwrites(self.channel)) - self.assertFalse(self.overwrite.send_messages) - self.assertFalse(self.overwrite.add_reactions) - self.channel.set_permissions.assert_awaited_once_with( + self.assertTrue(await self.cog._set_silence_overwrites(self.text_channel)) + self.assertFalse(self.text_overwrite.send_messages) + self.assertFalse(self.text_overwrite.add_reactions) + self.text_channel.set_permissions.assert_awaited_once_with( self.cog._verified_msg_role, - overwrite=self.overwrite + overwrite=self.text_overwrite ) async def test_preserved_other_overwrites(self): """Channel's other unrelated overwrites were not changed.""" - prev_overwrite_dict = dict(self.overwrite) - await self.cog._set_silence_overwrites(self.channel) - new_overwrite_dict = dict(self.overwrite) + prev_overwrite_dict = dict(self.text_overwrite) + await self.cog._set_silence_overwrites(self.text_channel) + new_overwrite_dict = dict(self.text_overwrite) # Remove 'send_messages' & 'add_reactions' keys because they were changed by the method. del prev_overwrite_dict['send_messages'] @@ -520,8 +524,8 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_cached_previous_overwrites(self): """Channel's previous overwrites were cached.""" overwrite_json = '{"send_messages": true, "add_reactions": false}' - await self.cog._set_silence_overwrites(self.channel) - self.cog.previous_overwrites.set.assert_called_once_with(self.channel.id, overwrite_json) + await self.cog._set_silence_overwrites(self.text_channel) + self.cog.previous_overwrites.set.assert_called_once_with(self.text_channel.id, overwrite_json) @autospec(silence, "datetime") async def test_cached_unsilence_time(self, datetime_mock): @@ -531,7 +535,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): timestamp = now_timestamp + duration * 60 datetime_mock.now.return_value = datetime.fromtimestamp(now_timestamp, tz=timezone.utc) - ctx = MockContext(channel=self.channel) + ctx = MockContext(channel=self.text_channel) await self.cog.silence.callback(self.cog, ctx, duration) self.cog.unsilence_timestamps.set.assert_awaited_once_with(ctx.channel.id, timestamp) @@ -539,13 +543,13 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_cached_indefinite_time(self): """A value of -1 was cached for a permanent silence.""" - ctx = MockContext(channel=self.channel) + ctx = MockContext(channel=self.text_channel) await self.cog.silence.callback(self.cog, ctx, None) self.cog.unsilence_timestamps.set.assert_awaited_once_with(ctx.channel.id, -1) async def test_scheduled_task(self): """An unsilence task was scheduled.""" - ctx = MockContext(channel=self.channel, invoke=mock.MagicMock()) + ctx = MockContext(channel=self.text_channel, invoke=mock.MagicMock()) await self.cog.silence.callback(self.cog, ctx, 5) @@ -555,10 +559,21 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_permanent_not_scheduled(self): """A task was not scheduled for a permanent silence.""" - ctx = MockContext(channel=self.channel) + ctx = MockContext(channel=self.text_channel) await self.cog.silence.callback(self.cog, ctx, None) self.cog.scheduler.schedule_later.assert_not_called() + async def test_correct_permission_updates(self): + """Tests if _set_silence_overwrites can correctly get and update permissions.""" + self.assertTrue(await self.cog._set_silence_overwrites(self.text_channel)) + self.assertFalse(self.text_overwrite.send_messages or self.text_overwrite.add_reactions) + + self.assertTrue(await self.cog._set_silence_overwrites(self.voice_channel)) + self.assertFalse(self.voice_overwrite.speak) + + self.assertTrue(await self.cog._set_silence_overwrites(self.voice_channel, True)) + self.assertFalse(self.voice_overwrite.speak or self.voice_overwrite.connect) + @autospec(silence.Silence, "unsilence_timestamps", pass_mocks=False) class UnsilenceTests(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From b2d88f860d5ca9d08c72be676a7bfdfd5943b417 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 24 Nov 2020 12:20:01 +0300 Subject: Removes AnyChannel Converter Removes the AnyChannel converter in favor of a combination of Text and Voice converters. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/converters.py | 40 ----------------------------------- bot/exts/moderation/silence.py | 6 +++--- tests/bot/test_converters.py | 48 ------------------------------------------ 3 files changed, 3 insertions(+), 91 deletions(-) (limited to 'tests') diff --git a/bot/converters.py b/bot/converters.py index 613be73eb..2e118d476 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -536,46 +536,6 @@ class FetchedUser(UserConverter): raise BadArgument(f"User `{arg}` does not exist") -class AnyChannelConverter(Converter): - """ - Converts to a `discord.Channel` or, raises an error. - - Unlike the default Channel Converter, this converter can handle channels given - in string, id, or mention formatting for both `TextChannel`s and `VoiceChannel`s. - Always returns 1 or fewer channels, errors if more than one match exists. - - It is able to handle the following formats (caveats noted below:) - 1. Convert from ID - Example: 267631170882240512 - 2. Convert from Explicit Mention - Example: #welcome - 3. Convert from ID Mention - Example: <#267631170882240512> - 4. Convert from Unmentioned Name: - Example: welcome - - All the previous conversions are valid for both text and voice channels, but explicit - raw names (#4) do not work for non-unique channels, instead opting for an error. - Explicit mentions (#2) do not work for non-unique voice channels either. - """ - - async def convert(self, ctx: Context, arg: str) -> t.Union[discord.TextChannel, discord.VoiceChannel]: - """Convert the `arg` to a `TextChannel` or `VoiceChannel`.""" - stripped = arg.strip().lstrip("<").lstrip("#").rstrip(">").lower() - - # Filter channels by name and ID - channels = [channel for channel in ctx.guild.channels if stripped in (channel.name.lower(), str(channel.id))] - - if len(channels) == 0: - # Couldn't find a matching channel - log.debug(f"Could not convert `{arg}` to channel, no matches found.") - raise BadArgument(f"{arg} returned no matches.") - - elif len(channels) > 1: - # Couldn't discern the desired channel - log.debug(f"Could not convert `{arg}` to channel, {len(channels)} matches found.") - raise BadArgument(f"The provided argument returned too many matches ({len(channels)}).") - - else: - return channels[0] - - def _snowflake_from_regex(pattern: t.Pattern, arg: str) -> int: """ Extract the snowflake from `arg` using a regex `pattern` and return it as an int. diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 62f3ede73..8ad30f0d9 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -12,7 +12,7 @@ from discord.ext.commands import Context from bot.bot import Bot from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles -from bot.converters import AnyChannelConverter, HushDurationConverter +from bot.converters import HushDurationConverter from bot.utils.lock import LockedResourceError, lock_arg from bot.utils.scheduling import Scheduler @@ -148,7 +148,7 @@ class Silence(commands.Cog): @lock_arg(LOCK_NAMESPACE, "ctx", attrgetter("channel"), raise_error=True) async def silence( self, ctx: Context, duration: HushDurationConverter = 10, kick: bool = False, - *, channel: Optional[AnyChannelConverter] = None + *, channel: Union[TextChannel, VoiceChannel] = None ) -> None: """ Silence the current channel for `duration` minutes or `forever`. @@ -182,7 +182,7 @@ class Silence(commands.Cog): await self.send_message(MSG_SILENCE_SUCCESS, ctx.channel, channel, True, duration) @commands.command(aliases=("unhush",)) - async def unsilence(self, ctx: Context, *, channel: AnyChannelConverter = None) -> None: + async def unsilence(self, ctx: Context, *, channel: Union[TextChannel, VoiceChannel] = None) -> None: """ Unsilence the given channel if given, else the current one. diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py index 6bea71977..c42111f3f 100644 --- a/tests/bot/test_converters.py +++ b/tests/bot/test_converters.py @@ -7,7 +7,6 @@ from dateutil.relativedelta import relativedelta from discord.ext.commands import BadArgument from bot.converters import ( - AnyChannelConverter, Duration, HushDurationConverter, ISODateTime, @@ -25,18 +24,6 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase): cls.context = MagicMock cls.context.author = 'bob' - cls.context.guild = MagicMock - cls.context.guild.channels = [] - - for i in range(10): - channel = MagicMock() - channel.name = f"test-{i}" - channel.id = str(i) * 18 - - cls.context.guild.channels.append(channel) - if i > 7: - cls.context.guild.channels.append(channel) - cls.fixed_utc_now = datetime.datetime.fromisoformat('2019-01-01T00:00:00') async def test_tag_content_converter_for_valid(self): @@ -325,38 +312,3 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase): with self.subTest(invalid_minutes_string=invalid_minutes_string, exception_message=exception_message): with self.assertRaisesRegex(BadArgument, re.escape(exception_message)): await converter.convert(self.context, invalid_minutes_string) - - async def test_any_channel_converter_for_valid(self): - """AnyChannelConverter returns correct channel from input.""" - test_values = ( - ("test-0", self.context.guild.channels[0]), - ("#test-0", self.context.guild.channels[0]), - (" #tEsT-0 ", self.context.guild.channels[0]), - (f"<#{'0' * 18}>", self.context.guild.channels[0]), - (f"{'0' * 18}", self.context.guild.channels[0]), - ("test-5", self.context.guild.channels[5]), - ("#test-5", self.context.guild.channels[5]), - (f"<#{'5' * 18}>", self.context.guild.channels[5]), - (f"{'5' * 18}", self.context.guild.channels[5]), - ) - - converter = AnyChannelConverter() - for input_string, expected_channel in test_values: - with self.subTest(input_string=input_string, expected_channel=expected_channel): - converted = await converter.convert(self.context, input_string) - self.assertEqual(expected_channel, converted) - - async def test_any_channel_converter_for_invalid(self): - """AnyChannelConverter raises BadArgument for invalid channels.""" - test_values = ( - ("#test-8", "The provided argument returned too many matches (2)."), - ("#test-9", "The provided argument returned too many matches (2)."), - ("#random-name", "#random-name returned no matches."), - ("test-10", "test-10 returned no matches.") - ) - - converter = AnyChannelConverter() - for invalid_input, exception_message in test_values: - with self.subTest(invalid_input=invalid_input, exception_message=exception_message): - with self.assertRaisesRegex(BadArgument, re.escape(exception_message)): - await converter.convert(self.context, invalid_input) -- cgit v1.2.3 From c37e9842602f6e8e7bb2cc0897a6c8e0988f7cff Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 24 Nov 2020 12:29:51 +0300 Subject: Fixes Typo in Doc Co-authored-by: Mark --- tests/bot/exts/moderation/test_silence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 2392e6e59..5c6e2d0f1 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -564,7 +564,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.cog.scheduler.schedule_later.assert_not_called() async def test_correct_permission_updates(self): - """Tests if _set_silence_overwrites can correctly get and update permissions.""" + """Tests if _set_silence_overwrites can correctly get and update permissions.""" self.assertTrue(await self.cog._set_silence_overwrites(self.text_channel)) self.assertFalse(self.text_overwrite.send_messages or self.text_overwrite.add_reactions) -- cgit v1.2.3 From 7cb3024e71cd81e9ef29f5f10cb2bc5fe62ad846 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 24 Nov 2020 13:19:51 +0300 Subject: Refractors SendMessage Function Refractors the send message function in silence to make it more understandable and flexible. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/moderation/silence.py | 31 ++++++++---------- tests/bot/exts/moderation/test_silence.py | 54 ++++++++++++++++++------------- 2 files changed, 46 insertions(+), 39 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 8ad30f0d9..7bc51ee93 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -26,7 +26,7 @@ MSG_SILENCE_SUCCESS = f"{Emojis.check_mark} silenced current channel for {{durat MSG_UNSILENCE_FAIL = f"{Emojis.cross_mark} current channel was not silenced." MSG_UNSILENCE_MANUAL = ( - f"{Emojis.cross_mark} current channel was not unsilenced because the channel overwrites were " + f"{Emojis.cross_mark} current channel was not unsilenced because the current overwrites were " f"set manually or the cache was prematurely cleared. " f"Please edit the overwrites manually to unsilence." ) @@ -119,30 +119,27 @@ class Silence(commands.Cog): async def send_message( self, message: str, source_channel: TextChannel, target_channel: Union[TextChannel, VoiceChannel], - alert_target: bool = False, duration: int = 0 + alert_target: bool = False ) -> None: """Helper function to send message confirmation to `source_channel`, and notification to `target_channel`.""" - await source_channel.send( - message.replace("current", target_channel.mention if source_channel != target_channel else "current") - .replace("{duration}", str(duration)) - ) - + # Get TextChannel connected to VoiceChannel if channel is of type voice voice_chat = None if isinstance(target_channel, VoiceChannel): - # Send to relevant channel voice_chat = await self._get_related_text_channel(target_channel) - if alert_target and source_channel != target_channel: - if isinstance(target_channel, VoiceChannel): - if voice_chat is None or voice_chat == source_channel: - return + # Reply to invocation channel + source_reply = message + if source_channel != target_channel: + source_reply = source_reply.replace("current channel", target_channel.mention) + await source_channel.send(source_reply) - await voice_chat.send( - message.replace("{duration}", str(duration)).replace("current", target_channel.mention) - ) + # Reply to target channel + if alert_target and source_channel != target_channel and source_channel != voice_chat: + if isinstance(target_channel, VoiceChannel) and (voice_chat is not None or voice_chat != source_channel): + await voice_chat.send(message.replace("current channel", target_channel.mention)) else: - await target_channel.send(message.replace("{duration}", str(duration))) + await target_channel.send(message) @commands.command(aliases=("hush",)) @lock_arg(LOCK_NAMESPACE, "ctx", attrgetter("channel"), raise_error=True) @@ -179,7 +176,7 @@ class Silence(commands.Cog): else: log.info(f"Silenced {channel_info} for {duration} minute(s).") - await self.send_message(MSG_SILENCE_SUCCESS, ctx.channel, channel, True, duration) + await self.send_message(MSG_SILENCE_SUCCESS.format(duration=duration), ctx.channel, channel, True) @commands.command(aliases=("unhush",)) async def unsilence(self, ctx: Context, *, channel: Union[TextChannel, VoiceChannel] = None) -> None: diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 5c6e2d0f1..70678d207 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -7,7 +7,7 @@ from unittest.mock import Mock from async_rediscache import RedisSession from discord import PermissionOverwrite -from bot.constants import Channels, Emojis, Guild, Roles +from bot.constants import Channels, Guild, Roles from bot.exts.moderation import silence from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockTextChannel, MockVoiceChannel, autospec @@ -198,42 +198,48 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): reset() with self.subTest("Replacement One Channel Test"): - await self.cog.send_message("The following should be replaced: current", - text_channel_1, text_channel_2, False) - text_channel_1.send.assert_called_once_with(f"The following should be replaced: {text_channel_1.mention}") + await self.cog.send_message( + "Current. The following should be replaced: current channel.", text_channel_1, text_channel_2, False + ) + + text_channel_1.send.assert_called_once_with( + f"Current. The following should be replaced: {text_channel_1.mention}." + ) + text_channel_2.send.assert_not_called() reset() with self.subTest("Replacement Two Channel Test"): - await self.cog.send_message("The following should be replaced: current", - text_channel_1, text_channel_2, True) - text_channel_1.send.assert_called_once_with(f"The following should be replaced: {text_channel_1.mention}") - text_channel_2.send.assert_called_once_with("The following should be replaced: current") + await self.cog.send_message( + "Current. The following should be replaced: current channel.", text_channel_1, text_channel_2, True + ) - reset() - with self.subTest("Replace Duration"): - await self.cog.send_message(f"{Emojis.check_mark} The following should be replaced: {{duration}}", - text_channel_1, text_channel_2, False) - text_channel_1.send.assert_called_once_with(f"{Emojis.check_mark} The following should be replaced: 0") - text_channel_2.send.assert_not_called() + text_channel_1.send.assert_called_once_with( + f"Current. The following should be replaced: {text_channel_1.mention}." + ) + + text_channel_2.send.assert_called_once_with("Current. The following should be replaced: current channel.") reset() with self.subTest("Text and Voice"): - await self.cog.send_message("This should show up just here", - text_channel_1, voice_channel, False) + await self.cog.send_message( + "This should show up just here", text_channel_1, voice_channel, False + ) text_channel_1.send.assert_called_once_with("This should show up just here") reset() with self.subTest("Text and Voice"): - await self.cog.send_message("This should show up as current", - text_channel_1, voice_channel, True) + await self.cog.send_message( + "This should show up as current channel", text_channel_1, voice_channel, True + ) text_channel_1.send.assert_called_once_with(f"This should show up as {voice_channel.mention}") text_channel_2.send.assert_called_once_with(f"This should show up as {voice_channel.mention}") reset() with self.subTest("Text and Voice Same Invocation"): - await self.cog.send_message("This should show up as current", - text_channel_2, voice_channel, True) + await self.cog.send_message( + "This should show up as current channel", text_channel_2, voice_channel, True + ) text_channel_2.send.assert_called_once_with(f"This should show up as {voice_channel.mention}") async def test_get_related_text_channel(self): @@ -455,7 +461,9 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): if ctx.channel == target or target is None: ctx.channel.send.assert_called_once_with(message) else: - ctx.channel.send.assert_called_once_with(message.replace("current", text_channel.mention)) + ctx.channel.send.assert_called_once_with( + message.replace("current channel", text_channel.mention) + ) target.send.assert_called_once_with(message) ctx.channel.send.reset_mock() @@ -643,7 +651,9 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): if ctx.channel == target or target is None: ctx.channel.send.assert_called_once_with(message) else: - ctx.channel.send.assert_called_once_with(message.replace("current", text_channel.mention)) + ctx.channel.send.assert_called_once_with( + message.replace("current channel", text_channel.mention) + ) target.send.assert_called_once_with(message) ctx.channel.send.reset_mock() -- cgit v1.2.3 From d2d7db69c4e54120d36146e2334f702f4259ed6b Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 24 Nov 2020 20:36:48 +0300 Subject: Moves VoiceChat Sync Out of Overwrites Function VoiceChat sync only needs to be called when the command is invoked, instead of while updating permissions. Moved call to command function to reflect that, and fixed failing tests. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/moderation/silence.py | 11 +++++++---- tests/bot/exts/moderation/test_silence.py | 12 +++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 7bc51ee93..64ffaa347 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -134,9 +134,10 @@ class Silence(commands.Cog): await source_channel.send(source_reply) # Reply to target channel - if alert_target and source_channel != target_channel and source_channel != voice_chat: - if isinstance(target_channel, VoiceChannel) and (voice_chat is not None or voice_chat != source_channel): - await voice_chat.send(message.replace("current channel", target_channel.mention)) + if alert_target and source_channel not in [target_channel, voice_chat]: + if isinstance(target_channel, VoiceChannel): + if voice_chat is not None: + await voice_chat.send(message.replace("current channel", target_channel.mention)) else: await target_channel.send(message) @@ -167,6 +168,9 @@ class Silence(commands.Cog): await self.send_message(MSG_SILENCE_FAIL, ctx.channel, channel) return + if isinstance(channel, VoiceChannel): + await self._force_voice_sync(channel, kick=kick) + await self._schedule_unsilence(ctx, channel, duration) if duration is None: @@ -244,7 +248,6 @@ class Silence(commands.Cog): overwrite.update(connect=False) await channel.set_permissions(self._verified_voice_role, overwrite=overwrite) - await self._force_voice_sync(channel, kick=kick) await self.previous_overwrites.set(channel.id, json.dumps(prev_overwrites)) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 70678d207..aab607392 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -446,11 +446,13 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_sent_to_correct_channel(self): """Test function sends messages to the correct channels.""" text_channel = MockTextChannel() + voice_channel = MockVoiceChannel() ctx = MockContext() test_cases = ( (None, silence.MSG_SILENCE_SUCCESS.format(duration=10)), (text_channel, silence.MSG_SILENCE_SUCCESS.format(duration=10)), + (voice_channel, silence.MSG_SILENCE_SUCCESS.format(duration=10)), (ctx.channel, silence.MSG_SILENCE_SUCCESS.format(duration=10)), ) @@ -460,14 +462,14 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog.silence.callback(self.cog, ctx, 10, False, channel=target) if ctx.channel == target or target is None: ctx.channel.send.assert_called_once_with(message) + else: - ctx.channel.send.assert_called_once_with( - message.replace("current channel", text_channel.mention) - ) - target.send.assert_called_once_with(message) + ctx.channel.send.assert_called_once_with(message.replace("current channel", target.mention)) + if isinstance(target, MockTextChannel): + target.send.assert_called_once_with(message) ctx.channel.send.reset_mock() - if target is not None: + if target is not None and isinstance(target, MockTextChannel): target.send.reset_mock() async def test_skipped_already_silenced(self): -- cgit v1.2.3 From 675d7504977acafdb73d6a51e91228180a7c02a2 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 24 Nov 2020 20:59:43 +0300 Subject: Fixes Typo in Silence Tests Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index aab607392..8f3c1cb8b 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -821,6 +821,6 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): channel.set_permissions.assert_called_once_with(role, overwrite=overwrites) if channel != ctx.channel: - ctx.channel.assert_not_called() + ctx.channel.send.assert_not_called() await reset() -- cgit v1.2.3 From 3ea66f3a61720258e0dda44fc59e547692375280 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 28 Nov 2020 20:13:54 +0300 Subject: Clarifies Constants Use in Silence Changes all usages of bot.constant to use dotted path to remove confusion and namespace collision. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/moderation/silence.py | 46 ++++++++++++++++--------------- tests/bot/exts/moderation/test_silence.py | 2 +- 2 files changed, 25 insertions(+), 23 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 8c71d422d..d1db0da9b 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -5,12 +5,12 @@ from datetime import datetime, timedelta, timezone from typing import Optional, Union from async_rediscache import RedisCache -from discord import Member, PermissionOverwrite, TextChannel, VoiceChannel +from discord import Guild, PermissionOverwrite, TextChannel, VoiceChannel from discord.ext import commands, tasks from discord.ext.commands import Context +from bot import constants from bot.bot import Bot -from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles from bot.converters import HushDurationConverter from bot.utils.lock import LockedResourceError, lock, lock_arg from bot.utils.scheduling import Scheduler @@ -19,17 +19,17 @@ log = logging.getLogger(__name__) LOCK_NAMESPACE = "silence" -MSG_SILENCE_FAIL = f"{Emojis.cross_mark} current channel is already silenced." -MSG_SILENCE_PERMANENT = f"{Emojis.check_mark} silenced current channel indefinitely." -MSG_SILENCE_SUCCESS = f"{Emojis.check_mark} silenced current channel for {{duration}} minute(s)." +MSG_SILENCE_FAIL = f"{constants.Emojis.cross_mark} current channel is already silenced." +MSG_SILENCE_PERMANENT = f"{constants.Emojis.check_mark} silenced current channel indefinitely." +MSG_SILENCE_SUCCESS = f"{constants.Emojis.check_mark} silenced current channel for {{duration}} minute(s)." -MSG_UNSILENCE_FAIL = f"{Emojis.cross_mark} current channel was not silenced." +MSG_UNSILENCE_FAIL = f"{constants.Emojis.cross_mark} current channel was not silenced." MSG_UNSILENCE_MANUAL = ( - f"{Emojis.cross_mark} current channel was not unsilenced because the current overwrites were " + f"{constants.Emojis.cross_mark} current channel was not unsilenced because the current overwrites were " f"set manually or the cache was prematurely cleared. " f"Please edit the overwrites manually to unsilence." ) -MSG_UNSILENCE_SUCCESS = f"{Emojis.check_mark} unsilenced current channel." +MSG_UNSILENCE_SUCCESS = f"{constants.Emojis.check_mark} unsilenced current channel." class SilenceNotifier(tasks.Loop): @@ -67,7 +67,9 @@ class SilenceNotifier(tasks.Loop): f"{channel.mention} for {(self._current_loop-start)//60} min" for channel, start in self._silenced_channels.items() ) - await self._alert_channel.send(f"<@&{Roles.moderators}> currently silenced channels: {channels_text}") + await self._alert_channel.send( + f"<@&{constants.Roles.moderators}> currently silenced channels: {channels_text}" + ) class Silence(commands.Cog): @@ -91,26 +93,26 @@ class Silence(commands.Cog): """Set instance attributes once the guild is available and reschedule unsilences.""" await self.bot.wait_until_guild_available() - guild = self.bot.get_guild(Guild.id) + guild = self.bot.get_guild(constants.Guild.id) - self._verified_msg_role = guild.get_role(Roles.verified) - self._verified_voice_role = guild.get_role(Roles.voice_verified) - self._helper_role = guild.get_role(Roles.helpers) + self._verified_msg_role = guild.get_role(constants.Roles.verified) + self._verified_voice_role = guild.get_role(constants.Roles.voice_verified) + self._helper_role = guild.get_role(constants.Roles.helpers) - self._mod_alerts_channel = self.bot.get_channel(Channels.mod_alerts) + self._mod_alerts_channel = self.bot.get_channel(constants.Channels.mod_alerts) - self.notifier = SilenceNotifier(self.bot.get_channel(Channels.mod_log)) + self.notifier = SilenceNotifier(self.bot.get_channel(constants.Channels.mod_log)) await self._reschedule() async def _get_related_text_channel(self, channel: VoiceChannel) -> Optional[TextChannel]: """Returns the text channel related to a voice channel.""" # TODO: Figure out a dynamic way of doing this channels = { - "off-topic": Channels.voice_chat, - "code/help 1": Channels.code_help_voice, - "code/help 2": Channels.code_help_voice_2, - "admin": Channels.admins_voice, - "staff": Channels.staff_voice + "off-topic": constants.Channels.voice_chat, + "code/help 1": constants.Channels.code_help_voice, + "code/help 2": constants.Channels.code_help_voice_2, + "admin": constants.Channels.admins_voice, + "staff": constants.Channels.staff_voice } for name in channels.keys(): if name in channel.name.lower(): @@ -360,7 +362,7 @@ class Silence(commands.Cog): # Alert Admin team if old overwrites were not available if prev_overwrites is None: await self._mod_alerts_channel.send( - f"<@&{Roles.admins}> Restored overwrites with default values after unsilencing " + f"<@&{constants.Roles.admins}> Restored overwrites with default values after unsilencing " f"{channel.mention}. Please check that the {permissions} " f"overwrites for {role.mention} are at their desired values." ) @@ -402,7 +404,7 @@ class Silence(commands.Cog): # This cannot be static (must have a __func__ attribute). async def cog_check(self, ctx: Context) -> bool: """Only allow moderators to invoke the commands in this cog.""" - return await commands.has_any_role(*MODERATION_ROLES).predicate(ctx) + return await commands.has_any_role(*constants.MODERATION_ROLES).predicate(ctx) def setup(bot: Bot) -> None: diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 8f3c1cb8b..bff2888b9 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -158,7 +158,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): self.assertTrue(self.cog._init_task.cancelled()) @autospec("discord.ext.commands", "has_any_role") - @mock.patch.object(silence, "MODERATION_ROLES", new=(1, 2, 3)) + @mock.patch.object(silence.constants, "MODERATION_ROLES", new=(1, 2, 3)) async def test_cog_check(self, role_check): """Role check was called with `MODERATION_ROLES`""" ctx = MockContext() -- cgit v1.2.3 From a6c8a9aca63f7d40d6c5701a08626da198c1d54a Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 28 Nov 2020 20:14:48 +0300 Subject: Refractors Voice Sync Helper Refractors the voice sync helper function into two different functions, one for each purpose. Moves the afk_channel get/creation code to its own function. Updates tests. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/moderation/silence.py | 88 +++++++++++---------- tests/bot/exts/moderation/test_silence.py | 124 ++++++++++++++++++------------ 2 files changed, 121 insertions(+), 91 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index d1db0da9b..9b3725326 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -174,7 +174,10 @@ class Silence(commands.Cog): return if isinstance(channel, VoiceChannel): - await self._force_voice_sync(channel, kick=kick) + if kick: + await self._kick_voice_members(channel) + else: + await self._force_voice_sync(channel) await self._schedule_unsilence(ctx, channel, duration) @@ -252,56 +255,57 @@ class Silence(commands.Cog): return True - async def _force_voice_sync( - self, channel: VoiceChannel, member: Optional[Member] = None, kick: bool = False - ) -> None: - """ - Move all non-staff members from `channel` to a temporary channel and back to force toggle role mute. - - If `member` is passed, the mute only occurs to that member. - Permission modification has to happen before this function. - - If `kick_all` is True, members will not be added back to the voice channel. - """ - # Handle member picking logic - if member is not None: - members = [member] - else: - members = channel.members - - # Handle kick logic - if kick: - for member in members: - await member.move_to(None, reason="Kicking voice channel member.") + @staticmethod + async def _get_afk_channel(guild: Guild) -> VoiceChannel: + """Get a guild's AFK channel, or create one if it does not exist.""" + afk_channel = guild.afk_channel - log.debug(f"Kicked all members from #{channel.name} ({channel.id}).") - return - - # Obtain temporary channel - afk_channel = channel.guild.afk_channel if afk_channel is None: overwrites = { - channel.guild.default_role: PermissionOverwrite(speak=False, connect=False, view_channel=False) + guild.default_role: PermissionOverwrite(speak=False, connect=False, view_channel=False) } - afk_channel = await channel.guild.create_voice_channel("mute-temp", overwrites=overwrites) + afk_channel = await guild.create_voice_channel("mute-temp", overwrites=overwrites) log.info(f"Failed to get afk-channel, created temporary channel #{afk_channel} ({afk_channel.id})") - # Schedule channel deletion in case function errors out - self.scheduler.schedule_later( - 30, afk_channel.id, afk_channel.delete(reason="Deleting temp mute channel.") - ) + return afk_channel - # Move all members to temporary channel and back - for member in members: - # Skip staff - if self._helper_role in member.roles: - continue + async def _kick_voice_members(self, channel: VoiceChannel) -> None: + """Remove all non-staff members from a voice channel.""" + log.debug(f"Removing all non staff members from #{channel.name} ({channel.id}).") + + for member in channel.members: + if self._helper_role not in member.roles: + await member.move_to(None, reason="Kicking member from voice channel.") - await member.move_to(afk_channel, reason="Muting member.") - log.debug(f"Moved {member.name} to afk channel.") + log.debug("Removed all members.") - await member.move_to(channel, reason="Muting member.") - log.debug(f"Moved {member.name} to original voice channel.") + async def _force_voice_sync(self, channel: VoiceChannel) -> None: + """ + Move all non-staff members from `channel` to a temporary channel and back to force toggle role mute. + + Permission modification has to happen before this function. + """ + # Obtain temporary channel + delete_channel = channel.guild.afk_channel is None + afk_channel = await self._get_afk_channel(channel.guild) + + try: + # Move all members to temporary channel and back + for member in channel.members: + # Skip staff + if self._helper_role in member.roles: + continue + + await member.move_to(afk_channel, reason="Muting VC member.") + log.debug(f"Moved {member.name} to afk channel.") + + await member.move_to(channel, reason="Muting VC member.") + log.debug(f"Moved {member.name} to original voice channel.") + + finally: + # Delete VC channel if it was created. + if delete_channel: + await afk_channel.delete(reason="Deleting temp mute channel.") async def _schedule_unsilence( self, ctx: Context, channel: Union[TextChannel, VoiceChannel], duration: Optional[int] diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index bff2888b9..9fb3e404a 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -2,7 +2,7 @@ import asyncio import unittest from datetime import datetime, timezone from unittest import mock -from unittest.mock import Mock +from unittest.mock import AsyncMock, Mock from async_rediscache import RedisSession from discord import PermissionOverwrite @@ -266,67 +266,69 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): """Tests the _force_voice_sync helper function.""" await self.cog._async_init() - afk_channel = MockVoiceChannel() - channel = MockVoiceChannel(guild=MockGuild(afk_channel=afk_channel)) - members = [] for _ in range(10): members.append(MockMember()) - channel.members = members - test_cases = ( - (members[0], False, "Muting member."), - (members[0], True, "Kicking voice channel member."), - (None, False, "Muting member."), - (None, True, "Kicking voice channel member."), - ) - - for member, kick, reason in test_cases: - with self.subTest(members=member, kick=kick, reason=reason): - await self.cog._force_voice_sync(channel, member, kick) - - for single_member in channel.members if member is None else [member]: - if kick: - single_member.move_to.assert_called_once_with(None, reason=reason) - else: - self.assertEqual(single_member.move_to.call_count, 2) - single_member.move_to.assert_has_calls([ - mock.call(afk_channel, reason=reason), - mock.call(channel, reason=reason) - ], any_order=False) + afk_channel = MockVoiceChannel() + channel = MockVoiceChannel(guild=MockGuild(afk_channel=afk_channel), members=members) - single_member.reset_mock() + await self.cog._force_voice_sync(channel) + for member in members: + self.assertEqual(member.move_to.call_count, 2) + member.move_to.assert_has_calls([ + mock.call(afk_channel, reason="Muting VC member."), + mock.call(channel, reason="Muting VC member.") + ], any_order=False) async def test_force_voice_sync_staff(self): """Tests to ensure _force_voice_sync does not kick staff members.""" await self.cog._async_init() member = MockMember(roles=[self.cog._helper_role]) - await self.cog._force_voice_sync(MockVoiceChannel(), member) + await self.cog._force_voice_sync(MockVoiceChannel(members=[member])) member.move_to.assert_not_called() async def test_force_voice_sync_no_channel(self): """Test to ensure _force_voice_sync can create its own voice channel if one is not available.""" await self.cog._async_init() - member = MockMember() channel = MockVoiceChannel(guild=MockGuild(afk_channel=None)) - - new_channel = MockVoiceChannel(delete=Mock()) + new_channel = MockVoiceChannel(delete=AsyncMock()) channel.guild.create_voice_channel.return_value = new_channel - with mock.patch.object(self.cog.scheduler, "schedule_later") as scheduler: - await self.cog._force_voice_sync(channel, member) + await self.cog._force_voice_sync(channel) + + # Check channel creation + overwrites = { + channel.guild.default_role: PermissionOverwrite(speak=False, connect=False, view_channel=False) + } + channel.guild.create_voice_channel.assert_called_once_with("mute-temp", overwrites=overwrites) + + # Check bot deleted channel + new_channel.delete.assert_called_once_with(reason="Deleting temp mute channel.") + + async def test_voice_kick(self): + """Test to ensure kick function can remove all members from a voice channel.""" + await self.cog._async_init() + + members = [] + for _ in range(10): + members.append(MockMember()) + + channel = MockVoiceChannel(members=members) + await self.cog._kick_voice_members(channel) + + for member in members: + member.move_to.assert_called_once_with(None, reason="Kicking member from voice channel.") - # Check channel creation - overwrites = { - channel.guild.default_role: PermissionOverwrite(speak=False, connect=False, view_channel=False) - } - channel.guild.create_voice_channel.assert_called_once_with("mute-temp", overwrites=overwrites) + async def test_voice_kick_staff(self): + """Test to ensure voice kick skips staff members.""" + await self.cog._async_init() + member = MockMember(roles=[self.cog._helper_role]) - # Check bot queued deletion - new_channel.delete.assert_called_once_with(reason="Deleting temp mute channel.") - scheduler.assert_called_once_with(30, new_channel.id, new_channel.delete()) + await self.cog._kick_voice_members(MockVoiceChannel(members=[member])) + member.move_to.assert_not_called() @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) @@ -457,21 +459,45 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): ) for target, message in test_cases: - with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=True): - with self.subTest(target_channel=target, message=message): - await self.cog.silence.callback(self.cog, ctx, 10, False, channel=target) - if ctx.channel == target or target is None: - ctx.channel.send.assert_called_once_with(message) + with mock.patch.object(self.cog, "_force_voice_sync") as voice_sync: + with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=True): + with self.subTest(target_channel=target, message=message): + await self.cog.silence.callback(self.cog, ctx, 10, False, channel=target) + if ctx.channel == target or target is None: + ctx.channel.send.assert_called_once_with(message) - else: - ctx.channel.send.assert_called_once_with(message.replace("current channel", target.mention)) - if isinstance(target, MockTextChannel): - target.send.assert_called_once_with(message) + else: + ctx.channel.send.assert_called_once_with(message.replace("current channel", target.mention)) + if isinstance(target, MockTextChannel): + target.send.assert_called_once_with(message) + else: + voice_sync.assert_called_once_with(target) ctx.channel.send.reset_mock() if target is not None and isinstance(target, MockTextChannel): target.send.reset_mock() + @mock.patch.object(silence.Silence, "_kick_voice_members") + @mock.patch.object(silence.Silence, "_force_voice_sync") + async def test_sync_or_kick_called(self, sync, kick): + """Tests if silence command calls kick or sync on voice channels when appropriate.""" + channel = MockVoiceChannel() + ctx = MockContext() + + with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=True): + with self.subTest("Test calls kick"): + await self.cog.silence.callback(self.cog, ctx, 10, kick=True, channel=channel) + kick.assert_called_once_with(channel) + sync.assert_not_called() + + kick.reset_mock() + sync.reset_mock() + + with self.subTest("Test calls sync"): + await self.cog.silence.callback(self.cog, ctx, 10, kick=False, channel=channel) + sync.assert_called_once_with(channel) + kick.assert_not_called() + async def test_skipped_already_silenced(self): """Permissions were not set and `False` was returned for an already silenced channel.""" subtests = ( -- cgit v1.2.3 From a8a8c104823fa1a23a9b33cd52c7c4e574d84330 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 28 Nov 2020 21:10:24 +0300 Subject: Refractors According To Style Guide Updates changes made in the PR to be more inline with style guide. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/moderation/silence.py | 32 ++++---- tests/bot/exts/moderation/test_silence.py | 120 ++++++++++++++---------------- 2 files changed, 74 insertions(+), 78 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 9b3725326..45c3f5b92 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -31,6 +31,8 @@ MSG_UNSILENCE_MANUAL = ( ) MSG_UNSILENCE_SUCCESS = f"{constants.Emojis.check_mark} unsilenced current channel." +TextOrVoiceChannel = Union[TextChannel, VoiceChannel] + class SilenceNotifier(tasks.Loop): """Loop notifier for posting notices to `alert_channel` containing added channels.""" @@ -40,7 +42,7 @@ class SilenceNotifier(tasks.Loop): self._silenced_channels = {} self._alert_channel = alert_channel - def add_channel(self, channel: Union[TextChannel, VoiceChannel]) -> None: + def add_channel(self, channel: TextOrVoiceChannel) -> None: """Add channel to `_silenced_channels` and start loop if not launched.""" if not self._silenced_channels: self.start() @@ -119,7 +121,10 @@ class Silence(commands.Cog): return self.bot.get_channel(channels[name]) async def send_message( - self, message: str, source_channel: TextChannel, target_channel: Union[TextChannel, VoiceChannel], + self, + message: str, + source_channel: TextChannel, + target_channel: TextOrVoiceChannel, alert_target: bool = False ) -> None: """Helper function to send message confirmation to `source_channel`, and notification to `target_channel`.""" @@ -138,7 +143,7 @@ class Silence(commands.Cog): elif source_channel != target_channel: await target_channel.send(message) - async def _select_lock_channel(*args) -> Union[TextChannel, VoiceChannel]: + async def _select_lock_channel(*args) -> TextOrVoiceChannel: """Passes the channel to be silenced to the resource lock.""" channel = args[0].get("channel") if channel is not None: @@ -150,8 +155,11 @@ class Silence(commands.Cog): @commands.command(aliases=("hush",)) @lock(LOCK_NAMESPACE, _select_lock_channel, raise_error=True) async def silence( - self, ctx: Context, duration: HushDurationConverter = 10, kick: bool = False, - *, channel: Union[TextChannel, VoiceChannel] = None + self, + ctx: Context, + duration: HushDurationConverter = 10, + kick: bool = False, + *, channel: TextOrVoiceChannel = None ) -> None: """ Silence the current channel for `duration` minutes or `forever`. @@ -191,7 +199,7 @@ class Silence(commands.Cog): await self.send_message(MSG_SILENCE_SUCCESS.format(duration=duration), ctx.channel, channel, True) @commands.command(aliases=("unhush",)) - async def unsilence(self, ctx: Context, *, channel: Union[TextChannel, VoiceChannel] = None) -> None: + async def unsilence(self, ctx: Context, *, channel: TextOrVoiceChannel = None) -> None: """ Unsilence the given channel if given, else the current one. @@ -204,9 +212,7 @@ class Silence(commands.Cog): await self._unsilence_wrapper(channel, ctx) @lock_arg(LOCK_NAMESPACE, "channel", raise_error=True) - async def _unsilence_wrapper( - self, channel: Union[TextChannel, VoiceChannel], ctx: Optional[Context] = None - ) -> None: + async def _unsilence_wrapper(self, channel: TextOrVoiceChannel, ctx: Optional[Context] = None) -> None: """Unsilence `channel` and send a success/failure message.""" msg_channel = channel if ctx is not None: @@ -229,7 +235,7 @@ class Silence(commands.Cog): else: await self.send_message(MSG_UNSILENCE_SUCCESS, msg_channel, channel, True) - async def _set_silence_overwrites(self, channel: Union[TextChannel, VoiceChannel], kick: bool = False) -> bool: + async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, kick: bool = False) -> bool: """Set silence permission overwrites for `channel` and return True if successful.""" # Get the original channel overwrites if isinstance(channel, TextChannel): @@ -307,9 +313,7 @@ class Silence(commands.Cog): if delete_channel: await afk_channel.delete(reason="Deleting temp mute channel.") - async def _schedule_unsilence( - self, ctx: Context, channel: Union[TextChannel, VoiceChannel], duration: Optional[int] - ) -> None: + async def _schedule_unsilence(self, ctx: Context, channel: TextOrVoiceChannel, duration: Optional[int]) -> None: """Schedule `ctx.channel` to be unsilenced if `duration` is not None.""" if duration is None: await self.unsilence_timestamps.set(channel.id, -1) @@ -318,7 +322,7 @@ class Silence(commands.Cog): unsilence_time = datetime.now(tz=timezone.utc) + timedelta(minutes=duration) await self.unsilence_timestamps.set(channel.id, unsilence_time.timestamp()) - async def _unsilence(self, channel: Union[TextChannel, VoiceChannel]) -> bool: + async def _unsilence(self, channel: TextOrVoiceChannel) -> bool: """ Unsilence `channel`. diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 9fb3e404a..635e017e3 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -198,49 +198,37 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): reset() with self.subTest("Replacement One Channel Test"): - await self.cog.send_message( - "Current. The following should be replaced: current channel.", text_channel_1, text_channel_2, False - ) - - text_channel_1.send.assert_called_once_with( - f"Current. The following should be replaced: {text_channel_1.mention}." - ) + message = "Current. The following should be replaced: current channel." + await self.cog.send_message(message, text_channel_1, text_channel_2, False) + text_channel_1.send.assert_called_once_with(message.replace("current channel", text_channel_1.mention)) text_channel_2.send.assert_not_called() reset() with self.subTest("Replacement Two Channel Test"): - await self.cog.send_message( - "Current. The following should be replaced: current channel.", text_channel_1, text_channel_2, True - ) - - text_channel_1.send.assert_called_once_with( - f"Current. The following should be replaced: {text_channel_1.mention}." - ) + message = "Current. The following should be replaced: current channel." + await self.cog.send_message(message, text_channel_1, text_channel_2, True) - text_channel_2.send.assert_called_once_with("Current. The following should be replaced: current channel.") + text_channel_1.send.assert_called_once_with(message.replace("current channel", text_channel_1.mention)) + text_channel_2.send.assert_called_once_with(message) reset() with self.subTest("Text and Voice"): - await self.cog.send_message( - "This should show up just here", text_channel_1, voice_channel, False - ) + await self.cog.send_message("This should show up just here", text_channel_1, voice_channel, False) text_channel_1.send.assert_called_once_with("This should show up just here") reset() with self.subTest("Text and Voice"): - await self.cog.send_message( - "This should show up as current channel", text_channel_1, voice_channel, True - ) - text_channel_1.send.assert_called_once_with(f"This should show up as {voice_channel.mention}") - text_channel_2.send.assert_called_once_with(f"This should show up as {voice_channel.mention}") + message = "This should show up as current channel" + await self.cog.send_message(message, text_channel_1, voice_channel, True) + text_channel_1.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) + text_channel_2.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) reset() with self.subTest("Text and Voice Same Invocation"): - await self.cog.send_message( - "This should show up as current channel", text_channel_2, voice_channel, True - ) - text_channel_2.send.assert_called_once_with(f"This should show up as {voice_channel.mention}") + message = "This should show up as current channel" + await self.cog.send_message(message, text_channel_2, voice_channel, True) + text_channel_2.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) async def test_get_related_text_channel(self): """Tests the helper function that connects voice to text channels.""" @@ -276,10 +264,11 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): await self.cog._force_voice_sync(channel) for member in members: self.assertEqual(member.move_to.call_count, 2) - member.move_to.assert_has_calls([ + calls = [ mock.call(afk_channel, reason="Muting VC member."), mock.call(channel, reason="Muting VC member.") - ], any_order=False) + ] + member.move_to.assert_has_calls(calls, any_order=False) async def test_force_voice_sync_staff(self): """Tests to ensure _force_voice_sync does not kick staff members.""" @@ -445,7 +434,9 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog.silence.callback(self.cog, ctx, duration) ctx.channel.send.assert_called_once_with(message) - async def test_sent_to_correct_channel(self): + @mock.patch.object(silence.Silence, "_set_silence_overwrites", return_value=True) + @mock.patch.object(silence.Silence, "_force_voice_sync") + async def test_sent_to_correct_channel(self, voice_sync, _): """Test function sends messages to the correct channels.""" text_channel = MockTextChannel() voice_channel = MockVoiceChannel() @@ -459,19 +450,17 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): ) for target, message in test_cases: - with mock.patch.object(self.cog, "_force_voice_sync") as voice_sync: - with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=True): - with self.subTest(target_channel=target, message=message): - await self.cog.silence.callback(self.cog, ctx, 10, False, channel=target) - if ctx.channel == target or target is None: - ctx.channel.send.assert_called_once_with(message) - - else: - ctx.channel.send.assert_called_once_with(message.replace("current channel", target.mention)) - if isinstance(target, MockTextChannel): - target.send.assert_called_once_with(message) - else: - voice_sync.assert_called_once_with(target) + with self.subTest(target_channel=target, message=message): + await self.cog.silence.callback(self.cog, ctx, 10, False, channel=target) + if ctx.channel == target or target is None: + ctx.channel.send.assert_called_once_with(message) + + else: + ctx.channel.send.assert_called_once_with(message.replace("current channel", target.mention)) + if isinstance(target, MockTextChannel): + target.send.assert_called_once_with(message) + else: + voice_sync.assert_called_once_with(target) ctx.channel.send.reset_mock() if target is not None and isinstance(target, MockTextChannel): @@ -771,7 +760,9 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) - async def test_unsilence_helper_fail(self): + @mock.patch.object(silence.Silence, "_unsilence", return_value=False) + @mock.patch.object(silence.Silence, "send_message") + async def test_unsilence_helper_fail(self, send_message, _): """Tests unsilence_wrapper when `_unsilence` fails.""" ctx = MockContext() @@ -797,16 +788,18 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): return self.val for context, channel, role, permission, message in test_cases: - with self.subTest(channel=channel, message=message): - with mock.patch.object(channel, "overwrites_for", return_value=PermClass(permission)) as overwrites: - with mock.patch.object(self.cog, "send_message") as send_message: - with mock.patch.object(self.cog, "_unsilence", return_value=False): - await self.cog._unsilence_wrapper(channel, context) + with mock.patch.object(channel, "overwrites_for", return_value=PermClass(permission)) as overwrites: + with self.subTest(channel=channel, message=message): + await self.cog._unsilence_wrapper(channel, context) + + overwrites.assert_called_once_with(role) + send_message.assert_called_once_with(message, ctx.channel, channel) - overwrites.assert_called_once_with(role) - send_message.assert_called_once_with(message, ctx.channel, channel) + send_message.reset_mock() - async def test_correct_overwrites(self): + @mock.patch.object(silence.Silence, "_force_voice_sync") + @mock.patch.object(silence.Silence, "send_message") + async def test_correct_overwrites(self, send_message, _): """Tests the overwrites returned by the _unsilence_wrapper are correct for voice and text channels.""" ctx = MockContext() @@ -822,6 +815,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): text_channel.reset_mock() voice_channel.reset_mock() + send_message.reset_mock() await reset() default_text_overwrites = text_channel.overwrites_for(text_role) @@ -836,17 +830,15 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): for context, channel, role, overwrites, message in test_cases: with self.subTest(ctx=context, channel=channel): - with mock.patch.object(self.cog, "send_message") as send_message: - with mock.patch.object(self.cog, "_force_voice_sync"): - await self.cog._unsilence_wrapper(channel, context) - - if context is None: - send_message.assert_called_once_with(message, channel, channel, True) - else: - send_message.assert_called_once_with(message, context.channel, channel, True) - - channel.set_permissions.assert_called_once_with(role, overwrite=overwrites) - if channel != ctx.channel: - ctx.channel.send.assert_not_called() + await self.cog._unsilence_wrapper(channel, context) + + if context is None: + send_message.assert_called_once_with(message, channel, channel, True) + else: + send_message.assert_called_once_with(message, context.channel, channel, True) + + channel.set_permissions.assert_called_once_with(role, overwrite=overwrites) + if channel != ctx.channel: + ctx.channel.send.assert_not_called() await reset() -- cgit v1.2.3 From ca266629c8083c261208ddba86ffe9e2a8b65bf3 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 1 Dec 2020 10:19:22 +0300 Subject: Fixes Voice Silence Tests Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 66 +++++++++++++++---------------- 1 file changed, 32 insertions(+), 34 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 635e017e3..90ddf6ad7 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -168,8 +168,8 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): role_check.assert_called_once_with(*(1, 2, 3)) role_check.return_value.predicate.assert_awaited_once_with(ctx) - @mock.patch.object(silence.Silence, "_get_related_text_channel") - async def test_send_message(self, mock_get_related_text_channel): + @mock.patch.object(silence, "VOICE_CHANNELS") + async def test_send_message(self, mock_voice_channels): """Test the send function reports to the correct channels.""" text_channel_1 = MockTextChannel() text_channel_2 = MockTextChannel() @@ -178,12 +178,13 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): voice_channel.name = "General/Offtopic" voice_channel.mention = f"#{voice_channel.name}" - mock_get_related_text_channel.return_value = text_channel_2 + mock_voice_channels.get.return_value = text_channel_2.id def reset(): text_channel_1.reset_mock() text_channel_2.reset_mock() voice_channel.reset_mock() + mock_voice_channels.reset_mock() with self.subTest("Basic One Channel Test"): await self.cog.send_message("Text basic message.", text_channel_1, text_channel_2, False) @@ -217,38 +218,29 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): await self.cog.send_message("This should show up just here", text_channel_1, voice_channel, False) text_channel_1.send.assert_called_once_with("This should show up just here") - reset() - with self.subTest("Text and Voice"): - message = "This should show up as current channel" - await self.cog.send_message(message, text_channel_1, voice_channel, True) - text_channel_1.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) - text_channel_2.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) + with mock.patch.object(self.cog, "bot") as bot_mock: + bot_mock.get_channel.return_value = text_channel_2 - reset() - with self.subTest("Text and Voice Same Invocation"): - message = "This should show up as current channel" - await self.cog.send_message(message, text_channel_2, voice_channel, True) - text_channel_2.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) - - async def test_get_related_text_channel(self): - """Tests the helper function that connects voice to text channels.""" - voice_channel = MockVoiceChannel() + reset() + with self.subTest("Text and Voice"): + message = "This should show up as current channel" + await self.cog.send_message(message, text_channel_1, voice_channel, True) + text_channel_1.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) + text_channel_2.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) - tests = ( - ("Off-Topic/General", Channels.voice_chat), - ("code/help 1", Channels.code_help_voice), - ("Staff", Channels.staff_voice), - ("ADMIN", Channels.admins_voice), - ("not in the channel list", None) - ) + mock_voice_channels.get.assert_called_once_with(voice_channel.id) + bot_mock.get_channel.assert_called_once_with(text_channel_2.id) + bot_mock.reset_mock() - with mock.patch.object(self.cog.bot, "get_channel", lambda x: x): - for (name, channel_id) in tests: - voice_channel.name = name - voice_channel.id = channel_id + reset() + with self.subTest("Text and Voice Same Invocation"): + message = "This should show up as current channel" + await self.cog.send_message(message, text_channel_2, voice_channel, True) + text_channel_2.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) - result_id = await self.cog._get_related_text_channel(voice_channel) - self.assertEqual(result_id, channel_id) + mock_voice_channels.get.assert_called_once_with(voice_channel.id) + bot_mock.get_channel.assert_called_once_with(text_channel_2.id) + bot_mock.reset_mock() async def test_force_voice_sync(self): """Tests the _force_voice_sync helper function.""" @@ -451,7 +443,10 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): for target, message in test_cases: with self.subTest(target_channel=target, message=message): - await self.cog.silence.callback(self.cog, ctx, 10, False, channel=target) + with mock.patch.object(self.cog, "bot") as bot_mock: + bot_mock.get_channel.return_value = AsyncMock() + await self.cog.silence.callback(self.cog, ctx, 10, False, channel=target) + if ctx.channel == target or target is None: ctx.channel.send.assert_called_once_with(message) @@ -466,14 +461,17 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): if target is not None and isinstance(target, MockTextChannel): target.send.reset_mock() + @mock.patch.object(silence.Silence, "_set_silence_overwrites", return_value=True) @mock.patch.object(silence.Silence, "_kick_voice_members") @mock.patch.object(silence.Silence, "_force_voice_sync") - async def test_sync_or_kick_called(self, sync, kick): + async def test_sync_or_kick_called(self, sync, kick, _): """Tests if silence command calls kick or sync on voice channels when appropriate.""" channel = MockVoiceChannel() ctx = MockContext() - with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=True): + with mock.patch.object(self.cog, "bot") as bot_mock: + bot_mock.get_channel.return_value = AsyncMock() + with self.subTest("Test calls kick"): await self.cog.silence.callback(self.cog, ctx, 10, kick=True, channel=channel) kick.assert_called_once_with(channel) -- cgit v1.2.3 From 68c2545e794fa284a957809a0cb1022740966118 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 29 Dec 2020 19:11:51 +0300 Subject: Refractors Helper Method Signatures Changes the signatures of a few helper methods to make them more concise and understandable. --- bot/exts/moderation/silence.py | 34 ++++++++++++++++--------------- tests/bot/exts/moderation/test_silence.py | 23 +++++++++++---------- 2 files changed, 30 insertions(+), 27 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index befcb2cc4..31103bc3e 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -2,7 +2,7 @@ import json import logging from contextlib import suppress from datetime import datetime, timedelta, timezone -from typing import Optional, Union +from typing import Optional, OrderedDict, Union from async_rediscache import RedisCache from discord import Guild, PermissionOverwrite, TextChannel, VoiceChannel @@ -81,6 +81,16 @@ class SilenceNotifier(tasks.Loop): ) +async def _select_lock_channel(args: OrderedDict[str, any]) -> TextOrVoiceChannel: + """Passes the channel to be silenced to the resource lock.""" + channel = args["channel"] + if channel is not None: + return channel + + else: + return args["ctx"].channel + + class Silence(commands.Cog): """Commands for stopping channel messages for `verified` role in a channel.""" @@ -118,7 +128,7 @@ class Silence(commands.Cog): message: str, source_channel: TextChannel, target_channel: TextOrVoiceChannel, - alert_target: bool = False + *, alert_target: bool = False ) -> None: """Helper function to send message confirmation to `source_channel`, and notification to `target_channel`.""" # Reply to invocation channel @@ -137,23 +147,14 @@ class Silence(commands.Cog): elif source_channel != target_channel: await target_channel.send(message) - async def _select_lock_channel(*args) -> TextOrVoiceChannel: - """Passes the channel to be silenced to the resource lock.""" - channel = args[0].get("channel") - if channel is not None: - return channel - - else: - return args[0].get("ctx").channel - @commands.command(aliases=("hush",)) @lock(LOCK_NAMESPACE, _select_lock_channel, raise_error=True) async def silence( self, ctx: Context, duration: HushDurationConverter = 10, - kick: bool = False, - *, channel: TextOrVoiceChannel = None + channel: TextOrVoiceChannel = None, + *, kick: bool = False ) -> None: """ Silence the current channel for `duration` minutes or `forever`. @@ -186,11 +187,12 @@ class Silence(commands.Cog): if duration is None: self.notifier.add_channel(channel) log.info(f"Silenced {channel_info} indefinitely.") - await self.send_message(MSG_SILENCE_PERMANENT, ctx.channel, channel, True) + await self.send_message(MSG_SILENCE_PERMANENT, ctx.channel, channel, alert_target=True) else: log.info(f"Silenced {channel_info} for {duration} minute(s).") - await self.send_message(MSG_SILENCE_SUCCESS.format(duration=duration), ctx.channel, channel, True) + formatted_message = MSG_SILENCE_SUCCESS.format(duration=duration) + await self.send_message(formatted_message, ctx.channel, channel, alert_target=True) @commands.command(aliases=("unhush",)) async def unsilence(self, ctx: Context, *, channel: TextOrVoiceChannel = None) -> None: @@ -227,7 +229,7 @@ class Silence(commands.Cog): await self.send_message(MSG_UNSILENCE_FAIL, msg_channel, channel) else: - await self.send_message(MSG_UNSILENCE_SUCCESS, msg_channel, channel, True) + await self.send_message(MSG_UNSILENCE_SUCCESS, msg_channel, channel, alert_target=True) async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, kick: bool = False) -> bool: """Set silence permission overwrites for `channel` and return True if successful.""" diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 31894761c..038e0a1a4 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -178,20 +178,20 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): mock_voice_channels.reset_mock() with self.subTest("Basic One Channel Test"): - await self.cog.send_message("Text basic message.", text_channel_1, text_channel_2, False) + await self.cog.send_message("Text basic message.", text_channel_1, text_channel_2, alert_target=False) text_channel_1.send.assert_called_once_with("Text basic message.") text_channel_2.send.assert_not_called() reset() with self.subTest("Basic Two Channel Test"): - await self.cog.send_message("Text basic message.", text_channel_1, text_channel_2, True) + await self.cog.send_message("Text basic message.", text_channel_1, text_channel_2, alert_target=True) text_channel_1.send.assert_called_once_with("Text basic message.") text_channel_2.send.assert_called_once_with("Text basic message.") reset() with self.subTest("Replacement One Channel Test"): message = "Current. The following should be replaced: current channel." - await self.cog.send_message(message, text_channel_1, text_channel_2, False) + await self.cog.send_message(message, text_channel_1, text_channel_2, alert_target=False) text_channel_1.send.assert_called_once_with(message.replace("current channel", text_channel_1.mention)) text_channel_2.send.assert_not_called() @@ -199,15 +199,16 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): reset() with self.subTest("Replacement Two Channel Test"): message = "Current. The following should be replaced: current channel." - await self.cog.send_message(message, text_channel_1, text_channel_2, True) + await self.cog.send_message(message, text_channel_1, text_channel_2, alert_target=True) text_channel_1.send.assert_called_once_with(message.replace("current channel", text_channel_1.mention)) text_channel_2.send.assert_called_once_with(message) reset() with self.subTest("Text and Voice"): - await self.cog.send_message("This should show up just here", text_channel_1, voice_channel, False) - text_channel_1.send.assert_called_once_with("This should show up just here") + message = "This should show up just here" + await self.cog.send_message(message, text_channel_1, voice_channel, alert_target=False) + text_channel_1.send.assert_called_once_with(message) with mock.patch.object(self.cog, "bot") as bot_mock: bot_mock.get_channel.return_value = text_channel_2 @@ -215,7 +216,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): reset() with self.subTest("Text and Voice"): message = "This should show up as current channel" - await self.cog.send_message(message, text_channel_1, voice_channel, True) + await self.cog.send_message(message, text_channel_1, voice_channel, alert_target=True) text_channel_1.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) text_channel_2.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) @@ -226,7 +227,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): reset() with self.subTest("Text and Voice Same Invocation"): message = "This should show up as current channel" - await self.cog.send_message(message, text_channel_2, voice_channel, True) + await self.cog.send_message(message, text_channel_2, voice_channel, alert_target=True) text_channel_2.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) mock_voice_channels.get.assert_called_once_with(voice_channel.id) @@ -436,7 +437,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): with self.subTest(target_channel=target, message=message): with mock.patch.object(self.cog, "bot") as bot_mock: bot_mock.get_channel.return_value = AsyncMock() - await self.cog.silence.callback(self.cog, ctx, 10, False, channel=target) + await self.cog.silence.callback(self.cog, ctx, 10, target, kick=False) if ctx.channel == target or target is None: ctx.channel.send.assert_called_once_with(message) @@ -822,9 +823,9 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._unsilence_wrapper(channel, context) if context is None: - send_message.assert_called_once_with(message, channel, channel, True) + send_message.assert_called_once_with(message, channel, channel, alert_target=True) else: - send_message.assert_called_once_with(message, context.channel, channel, True) + send_message.assert_called_once_with(message, context.channel, channel, alert_target=True) channel.set_permissions.assert_called_once_with(role, overwrite=overwrites) if channel != ctx.channel: -- cgit v1.2.3 From 1b747fccd16d2667c6f4129a222cd9ea3eda5602 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 29 Dec 2020 19:18:03 +0300 Subject: Makes Kick Keyword Only Parameter --- bot/exts/moderation/silence.py | 4 ++-- tests/bot/exts/moderation/test_silence.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 157c150fd..e91e558ec 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -171,7 +171,7 @@ class Silence(commands.Cog): channel_info = f"#{channel} ({channel.id})" log.debug(f"{ctx.author} is silencing channel {channel_info}.") - if not await self._set_silence_overwrites(channel, kick): + if not await self._set_silence_overwrites(channel, kick=kick): log.info(f"Tried to silence channel {channel_info} but the channel was already silenced.") await self.send_message(MSG_SILENCE_FAIL, ctx.channel, channel) return @@ -231,7 +231,7 @@ class Silence(commands.Cog): else: await self.send_message(MSG_UNSILENCE_SUCCESS, msg_channel, channel, alert_target=True) - async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, kick: bool = False) -> bool: + async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, *, kick: bool = False) -> bool: """Set silence permission overwrites for `channel` and return True if successful.""" # Get the original channel overwrites if isinstance(channel, TextChannel): diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 038e0a1a4..44c3620ac 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -586,7 +586,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.assertTrue(await self.cog._set_silence_overwrites(self.voice_channel)) self.assertFalse(self.voice_overwrite.speak) - self.assertTrue(await self.cog._set_silence_overwrites(self.voice_channel, True)) + self.assertTrue(await self.cog._set_silence_overwrites(self.voice_channel, kick=True)) self.assertFalse(self.voice_overwrite.speak or self.voice_overwrite.connect) -- cgit v1.2.3 From 9d3fba1c3143a529e32bd660696922c3ff902d16 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 29 Dec 2020 20:06:51 +0300 Subject: Breaks Out Send Message Tests Moves the tests for the helper method `send_message` to simplify tests, and avoid repeated code. --- tests/bot/exts/moderation/test_silence.py | 156 ++++++++++++++++-------------- 1 file changed, 81 insertions(+), 75 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 44c3620ac..8f4574d13 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -159,81 +159,6 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): role_check.assert_called_once_with(*(1, 2, 3)) role_check.return_value.predicate.assert_awaited_once_with(ctx) - @mock.patch.object(silence, "VOICE_CHANNELS") - async def test_send_message(self, mock_voice_channels): - """Test the send function reports to the correct channels.""" - text_channel_1 = MockTextChannel() - text_channel_2 = MockTextChannel() - - voice_channel = MockVoiceChannel() - voice_channel.name = "General/Offtopic" - voice_channel.mention = f"#{voice_channel.name}" - - mock_voice_channels.get.return_value = text_channel_2.id - - def reset(): - text_channel_1.reset_mock() - text_channel_2.reset_mock() - voice_channel.reset_mock() - mock_voice_channels.reset_mock() - - with self.subTest("Basic One Channel Test"): - await self.cog.send_message("Text basic message.", text_channel_1, text_channel_2, alert_target=False) - text_channel_1.send.assert_called_once_with("Text basic message.") - text_channel_2.send.assert_not_called() - - reset() - with self.subTest("Basic Two Channel Test"): - await self.cog.send_message("Text basic message.", text_channel_1, text_channel_2, alert_target=True) - text_channel_1.send.assert_called_once_with("Text basic message.") - text_channel_2.send.assert_called_once_with("Text basic message.") - - reset() - with self.subTest("Replacement One Channel Test"): - message = "Current. The following should be replaced: current channel." - await self.cog.send_message(message, text_channel_1, text_channel_2, alert_target=False) - - text_channel_1.send.assert_called_once_with(message.replace("current channel", text_channel_1.mention)) - text_channel_2.send.assert_not_called() - - reset() - with self.subTest("Replacement Two Channel Test"): - message = "Current. The following should be replaced: current channel." - await self.cog.send_message(message, text_channel_1, text_channel_2, alert_target=True) - - text_channel_1.send.assert_called_once_with(message.replace("current channel", text_channel_1.mention)) - text_channel_2.send.assert_called_once_with(message) - - reset() - with self.subTest("Text and Voice"): - message = "This should show up just here" - await self.cog.send_message(message, text_channel_1, voice_channel, alert_target=False) - text_channel_1.send.assert_called_once_with(message) - - with mock.patch.object(self.cog, "bot") as bot_mock: - bot_mock.get_channel.return_value = text_channel_2 - - reset() - with self.subTest("Text and Voice"): - message = "This should show up as current channel" - await self.cog.send_message(message, text_channel_1, voice_channel, alert_target=True) - text_channel_1.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) - text_channel_2.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) - - mock_voice_channels.get.assert_called_once_with(voice_channel.id) - bot_mock.get_channel.assert_called_once_with(text_channel_2.id) - bot_mock.reset_mock() - - reset() - with self.subTest("Text and Voice Same Invocation"): - message = "This should show up as current channel" - await self.cog.send_message(message, text_channel_2, voice_channel, alert_target=True) - text_channel_2.send.assert_called_once_with(message.replace("current channel", voice_channel.mention)) - - mock_voice_channels.get.assert_called_once_with(voice_channel.id) - bot_mock.get_channel.assert_called_once_with(text_channel_2.id) - bot_mock.reset_mock() - async def test_force_voice_sync(self): """Tests the _force_voice_sync helper function.""" await self.cog._async_init() @@ -832,3 +757,84 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): ctx.channel.send.assert_not_called() await reset() + + +class SendMessageTests(unittest.IsolatedAsyncioTestCase): + """Unittests for the send message helper function.""" + + def setUp(self) -> None: + self.bot = MockBot() + self.cog = silence.Silence(self.bot) + + self.text_channels = [MockTextChannel() for _ in range(2)] + self.bot.get_channel.return_value = self.text_channels[1] + + self.voice_channel = MockVoiceChannel(name="General/Offtopic") + + async def test_send_to_channel(self): + """Tests a basic case for the send function.""" + message = "Test basic message." + await self.cog.send_message(message, *self.text_channels, alert_target=False) + + self.text_channels[0].send.assert_called_once_with(message) + self.text_channels[1].send.assert_not_called() + + async def test_send_to_multiple_channels(self): + """Tests sending messages to two channels.""" + message = "Test basic message." + await self.cog.send_message(message, *self.text_channels, alert_target=True) + + self.text_channels[0].send.assert_called_once_with(message) + self.text_channels[1].send.assert_called_once_with(message) + + async def test_duration_replacement(self): + """Tests that the channel name was set correctly for one target channel.""" + message = "Current. The following should be replaced: current channel." + await self.cog.send_message(message, *self.text_channels, alert_target=False) + + updated_message = message.replace("current channel", self.text_channels[0].mention) + self.text_channels[0].send.assert_called_once_with(updated_message) + self.text_channels[1].send.assert_not_called() + + async def test_name_replacement_multiple_channels(self): + """Tests that the channel name was set correctly for two channels.""" + message = "Current. The following should be replaced: current channel." + await self.cog.send_message(message, *self.text_channels, alert_target=True) + + updated_message = message.replace("current channel", self.text_channels[0].mention) + self.text_channels[0].send.assert_called_once_with(updated_message) + self.text_channels[1].send.assert_called_once_with(message) + + async def test_silence_voice(self): + """Tests that the correct message was sent when a voice channel is muted without alerting.""" + message = "This should show up just here." + await self.cog.send_message(message, self.text_channels[0], self.voice_channel, alert_target=False) + self.text_channels[0].send.assert_called_once_with(message) + + async def test_silence_voice_alert(self): + """Tests that the correct message was sent when a voice channel is muted with alerts.""" + with unittest.mock.patch.object(silence, "VOICE_CHANNELS") as mock_voice_channels: + mock_voice_channels.get.return_value = self.text_channels[1].id + + message = "This should show up as current channel." + await self.cog.send_message(message, self.text_channels[0], self.voice_channel, alert_target=True) + + updated_message = message.replace("current channel", self.voice_channel.mention) + self.text_channels[0].send.assert_called_once_with(updated_message) + self.text_channels[1].send.assert_called_once_with(updated_message) + + mock_voice_channels.get.assert_called_once_with(self.voice_channel.id) + + async def test_silence_voice_sibling_channel(self): + """Tests silencing a voice channel from the related text channel.""" + with unittest.mock.patch.object(silence, "VOICE_CHANNELS") as mock_voice_channels: + mock_voice_channels.get.return_value = self.text_channels[1].id + + message = "This should show up as current channel." + await self.cog.send_message(message, self.text_channels[1], self.voice_channel, alert_target=True) + + updated_message = message.replace("current channel", self.voice_channel.mention) + self.text_channels[1].send.assert_called_once_with(updated_message) + + mock_voice_channels.get.assert_called_once_with(self.voice_channel.id) + self.bot.get_channel.assert_called_once_with(self.text_channels[1].id) -- cgit v1.2.3 From 70fcd2b68706ae5e8e35407beaa04e2895f3dae8 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sun, 10 Jan 2021 16:22:15 +0300 Subject: Cleans Up & Simplifies Tests Cleans up the silence tests by removing unneeded or repeated mocks. Simplifies tests where possible by joining similar tests. --- bot/exts/moderation/silence.py | 4 +- tests/bot/exts/moderation/test_silence.py | 111 +++++++++--------------------- 2 files changed, 35 insertions(+), 80 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 26aa77b61..1a3c48394 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -229,9 +229,9 @@ class Silence(commands.Cog): # Send fail message to muted channel or voice chat channel, and invocation channel if manual: - await self.send_message(MSG_UNSILENCE_MANUAL, msg_channel, channel) + await self.send_message(MSG_UNSILENCE_MANUAL, msg_channel, channel, alert_target=False) else: - await self.send_message(MSG_UNSILENCE_FAIL, msg_channel, channel) + await self.send_message(MSG_UNSILENCE_FAIL, msg_channel, channel, alert_target=False) else: await self.send_message(MSG_UNSILENCE_SUCCESS, msg_channel, channel, alert_target=True) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 8f4574d13..5505d7a53 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -163,29 +163,22 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): """Tests the _force_voice_sync helper function.""" await self.cog._async_init() - members = [] - for _ in range(10): - members.append(MockMember()) + members = [MockMember() for _ in range(10)] + members.extend([MockMember(roles=[self.cog._helper_role]) for _ in range(3)]) - afk_channel = MockVoiceChannel() - channel = MockVoiceChannel(guild=MockGuild(afk_channel=afk_channel), members=members) + channel = MockVoiceChannel(members=members) await self.cog._force_voice_sync(channel) for member in members: - self.assertEqual(member.move_to.call_count, 2) - calls = [ - mock.call(afk_channel, reason="Muting VC member."), - mock.call(channel, reason="Muting VC member.") - ] - member.move_to.assert_has_calls(calls, any_order=False) - - async def test_force_voice_sync_staff(self): - """Tests to ensure _force_voice_sync does not kick staff members.""" - await self.cog._async_init() - member = MockMember(roles=[self.cog._helper_role]) + if self.cog._helper_role in member.roles: + member.move_to.assert_not_called() + else: + self.assertEqual(member.move_to.call_count, 2) + calls = member.move_to.call_args_list - await self.cog._force_voice_sync(MockVoiceChannel(members=[member])) - member.move_to.assert_not_called() + # Tests that the member was moved to the afk channel, and back. + self.assertEqual((channel.guild.afk_channel,), calls[0].args) + self.assertEqual((channel,), calls[1].args) async def test_force_voice_sync_no_channel(self): """Test to ensure _force_voice_sync can create its own voice channel if one is not available.""" @@ -204,29 +197,23 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): channel.guild.create_voice_channel.assert_called_once_with("mute-temp", overwrites=overwrites) # Check bot deleted channel - new_channel.delete.assert_called_once_with(reason="Deleting temp mute channel.") + new_channel.delete.assert_called_once() async def test_voice_kick(self): """Test to ensure kick function can remove all members from a voice channel.""" await self.cog._async_init() - members = [] - for _ in range(10): - members.append(MockMember()) + members = [MockMember() for _ in range(10)] + members.extend([MockMember(roles=[self.cog._helper_role]) for _ in range(3)]) channel = MockVoiceChannel(members=members) await self.cog._kick_voice_members(channel) for member in members: - member.move_to.assert_called_once_with(None, reason="Kicking member from voice channel.") - - async def test_voice_kick_staff(self): - """Test to ensure voice kick skips staff members.""" - await self.cog._async_init() - member = MockMember(roles=[self.cog._helper_role]) - - await self.cog._kick_voice_members(MockVoiceChannel(members=[member])) - member.move_to.assert_not_called() + if self.cog._helper_role in member.roles: + member.move_to.assert_not_called() + else: + self.assertEqual((None,), member.move_to.call_args_list[0].args) @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) @@ -311,7 +298,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): @autospec(silence.Silence, "_reschedule", pass_mocks=False) @autospec(silence, "Scheduler", "SilenceNotifier", pass_mocks=False) def setUp(self) -> None: - self.bot = MockBot() + self.bot = MockBot(get_channel=lambda _: MockTextChannel()) self.cog = silence.Silence(self.bot) self.cog._init_task = asyncio.Future() self.cog._init_task.set_result(None) @@ -360,9 +347,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): for target, message in test_cases: with self.subTest(target_channel=target, message=message): - with mock.patch.object(self.cog, "bot") as bot_mock: - bot_mock.get_channel.return_value = AsyncMock() - await self.cog.silence.callback(self.cog, ctx, 10, target, kick=False) + await self.cog.silence.callback(self.cog, ctx, 10, target, kick=False) if ctx.channel == target or target is None: ctx.channel.send.assert_called_once_with(message) @@ -505,9 +490,6 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_correct_permission_updates(self): """Tests if _set_silence_overwrites can correctly get and update permissions.""" - self.assertTrue(await self.cog._set_silence_overwrites(self.text_channel)) - self.assertFalse(self.text_overwrite.send_messages or self.text_overwrite.add_reactions) - self.assertTrue(await self.cog._set_silence_overwrites(self.voice_channel)) self.assertFalse(self.voice_overwrite.speak) @@ -548,49 +530,22 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): (False, silence.MSG_UNSILENCE_MANUAL, PermissionOverwrite(send_messages=False)), (False, silence.MSG_UNSILENCE_MANUAL, PermissionOverwrite(add_reactions=False)), ) + for was_unsilenced, message, overwrite in test_cases: ctx = MockContext() - with self.subTest(was_unsilenced=was_unsilenced, message=message, overwrite=overwrite): - with mock.patch.object(self.cog, "_unsilence", return_value=was_unsilenced): - ctx.channel.overwrites_for.return_value = overwrite - await self.cog.unsilence.callback(self.cog, ctx) - ctx.channel.send.assert_called_once_with(message) - - async def test_sent_to_correct_channel(self): - """Test function sends messages to the correct channels.""" - unsilenced_overwrite = PermissionOverwrite(send_messages=True, add_reactions=True) - text_channel = MockTextChannel() - ctx = MockContext() - - test_cases = ( - (None, silence.MSG_UNSILENCE_SUCCESS.format(duration=10)), - (text_channel, silence.MSG_UNSILENCE_SUCCESS.format(duration=10)), - (ctx.channel, silence.MSG_UNSILENCE_SUCCESS.format(duration=10)), - ) - for target, message in test_cases: - with self.subTest(target_channel=target, message=message): - with mock.patch.object(self.cog, "_unsilence", return_value=True): - # Assign Return - if ctx.channel == target or target is None: - ctx.channel.overwrites_for.return_value = unsilenced_overwrite - else: - target.overwrites_for.return_value = unsilenced_overwrite + for target in [None, MockTextChannel()]: + ctx.channel.overwrites_for.return_value = overwrite + if target: + target.overwrites_for.return_value = overwrite - await self.cog.unsilence.callback(self.cog, ctx, channel=target) + with mock.patch.object(self.cog, "_unsilence", return_value=was_unsilenced): + with mock.patch.object(self.cog, "send_message") as send_message: + with self.subTest(was_unsilenced=was_unsilenced, overwrite=overwrite, target=target): + await self.cog.unsilence.callback(self.cog, ctx, channel=target) - # Check Messages - if ctx.channel == target or target is None: - ctx.channel.send.assert_called_once_with(message) - else: - ctx.channel.send.assert_called_once_with( - message.replace("current channel", text_channel.mention) - ) - target.send.assert_called_once_with(message) - - ctx.channel.send.reset_mock() - if target is not None: - target.send.reset_mock() + call_args = (message, ctx.channel, target or ctx.channel) + send_message.assert_called_once_with(*call_args, alert_target=was_unsilenced) async def test_skipped_already_unsilenced(self): """Permissions were not set and `False` was returned for an already unsilenced channel.""" @@ -708,7 +663,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._unsilence_wrapper(channel, context) overwrites.assert_called_once_with(role) - send_message.assert_called_once_with(message, ctx.channel, channel) + send_message.assert_called_once_with(message, ctx.channel, channel, alert_target=False) send_message.reset_mock() @@ -769,7 +724,7 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): self.text_channels = [MockTextChannel() for _ in range(2)] self.bot.get_channel.return_value = self.text_channels[1] - self.voice_channel = MockVoiceChannel(name="General/Offtopic") + self.voice_channel = MockVoiceChannel() async def test_send_to_channel(self): """Tests a basic case for the send function.""" -- cgit v1.2.3 From 52c69c5d51d9938cd7a56bf5cbc2c26a371883d9 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sun, 10 Jan 2021 16:23:32 +0300 Subject: Cleans Up Voice Sync Tests Cleans up the tests related to the voice sync/kick functions by adding a helper method to simplify mocking. --- tests/bot/exts/moderation/test_silence.py | 59 +++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 18 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 5505d7a53..a365b2aae 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -291,6 +291,17 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): self.cog.notifier.add_channel.assert_not_called() +def voice_sync_helper(function): + """Helper wrapper to test the sync and kick functions for voice channels.""" + @autospec(silence.Silence, "_force_voice_sync", "_kick_voice_members", "_set_silence_overwrites") + async def inner(self, sync, kick, overwrites): + overwrites.return_value = True + await function(self, MockContext(), + sync, kick) + + return inner + + @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) class SilenceTests(unittest.IsolatedAsyncioTestCase): """Tests for the silence command and its related helper methods.""" @@ -363,29 +374,41 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): if target is not None and isinstance(target, MockTextChannel): target.send.reset_mock() - @mock.patch.object(silence.Silence, "_set_silence_overwrites", return_value=True) - @mock.patch.object(silence.Silence, "_kick_voice_members") - @mock.patch.object(silence.Silence, "_force_voice_sync") - async def test_sync_or_kick_called(self, sync, kick, _): - """Tests if silence command calls kick or sync on voice channels when appropriate.""" + @voice_sync_helper + async def test_sync_called(self, ctx, sync, kick): + """Tests if silence command calls sync on a voice channel.""" channel = MockVoiceChannel() - ctx = MockContext() + await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=False) - with mock.patch.object(self.cog, "bot") as bot_mock: - bot_mock.get_channel.return_value = AsyncMock() + sync.assert_called_once_with(self.cog, channel) + kick.assert_not_called() - with self.subTest("Test calls kick"): - await self.cog.silence.callback(self.cog, ctx, 10, kick=True, channel=channel) - kick.assert_called_once_with(channel) - sync.assert_not_called() + @voice_sync_helper + async def test_kick_called(self, ctx, sync, kick): + """Tests if silence command calls kick on a voice channel.""" + channel = MockVoiceChannel() + await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=True) + + kick.assert_called_once_with(self.cog, channel) + sync.assert_not_called() + + @voice_sync_helper + async def test_sync_not_called(self, ctx, sync, kick): + """Tests that silence command does not call sync on a text channel.""" + channel = MockTextChannel() + await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=False) + + sync.assert_not_called() + kick.assert_not_called() - kick.reset_mock() - sync.reset_mock() + @voice_sync_helper + async def test_kick_not_called(self, ctx, sync, kick): + """Tests that silence command does not call kick on a text channel.""" + channel = MockTextChannel() + await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=True) - with self.subTest("Test calls sync"): - await self.cog.silence.callback(self.cog, ctx, 10, kick=False, channel=channel) - sync.assert_called_once_with(channel) - kick.assert_not_called() + sync.assert_not_called() + kick.assert_not_called() async def test_skipped_already_silenced(self): """Permissions were not set and `False` was returned for an already silenced channel.""" -- cgit v1.2.3 From 1a1a283d617592fbf3995d7cd5e1d88be75f92ea Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 4 Feb 2021 12:29:59 +0300 Subject: Updates Voice Kick Restriction Tests --- bot/exts/moderation/silence.py | 4 ++-- tests/bot/exts/moderation/test_silence.py | 26 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 4253cd4f3..ea531c37a 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -117,7 +117,6 @@ class Silence(commands.Cog): self._everyone_role = guild.default_role self._verified_voice_role = guild.get_role(constants.Roles.voice_verified) - self._helper_role = guild.get_role(constants.Roles.helpers) self._mod_alerts_channel = self.bot.get_channel(constants.Channels.mod_alerts) @@ -277,7 +276,8 @@ class Silence(commands.Cog): return afk_channel - async def _kick_voice_members(self, channel: VoiceChannel) -> None: + @staticmethod + async def _kick_voice_members(channel: VoiceChannel) -> None: """Remove all non-staff members from a voice channel.""" log.debug(f"Removing all non staff members from #{channel.name} ({channel.id}).") diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index a365b2aae..2d85af7e0 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -7,9 +7,18 @@ from unittest.mock import AsyncMock, Mock from async_rediscache import RedisSession from discord import PermissionOverwrite -from bot.constants import Channels, Guild, Roles +from bot.constants import Channels, Guild, MODERATION_ROLES, Roles from bot.exts.moderation import silence -from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockTextChannel, MockVoiceChannel, autospec +from tests.helpers import ( + MockBot, + MockContext, + MockGuild, + MockMember, + MockRole, + MockTextChannel, + MockVoiceChannel, + autospec +) redis_session = None redis_loop = asyncio.get_event_loop() @@ -164,13 +173,13 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): await self.cog._async_init() members = [MockMember() for _ in range(10)] - members.extend([MockMember(roles=[self.cog._helper_role]) for _ in range(3)]) + members.extend([MockMember(roles=[MockRole(id=role)]) for role in MODERATION_ROLES]) channel = MockVoiceChannel(members=members) await self.cog._force_voice_sync(channel) for member in members: - if self.cog._helper_role in member.roles: + if any(role.id in MODERATION_ROLES for role in member.roles): member.move_to.assert_not_called() else: self.assertEqual(member.move_to.call_count, 2) @@ -204,13 +213,13 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): await self.cog._async_init() members = [MockMember() for _ in range(10)] - members.extend([MockMember(roles=[self.cog._helper_role]) for _ in range(3)]) + members.extend([MockMember(roles=[MockRole(id=role)]) for role in MODERATION_ROLES]) channel = MockVoiceChannel(members=members) await self.cog._kick_voice_members(channel) for member in members: - if self.cog._helper_role in member.roles: + if any(role.id in MODERATION_ROLES for role in member.roles): member.move_to.assert_not_called() else: self.assertEqual((None,), member.move_to.call_args_list[0].args) @@ -296,8 +305,7 @@ def voice_sync_helper(function): @autospec(silence.Silence, "_force_voice_sync", "_kick_voice_members", "_set_silence_overwrites") async def inner(self, sync, kick, overwrites): overwrites.return_value = True - await function(self, MockContext(), - sync, kick) + await function(self, MockContext(), sync, kick) return inner @@ -389,7 +397,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): channel = MockVoiceChannel() await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=True) - kick.assert_called_once_with(self.cog, channel) + kick.assert_called_once_with(channel) sync.assert_not_called() @voice_sync_helper -- cgit v1.2.3 From baa907f5ef056ec3001759cc6f9a9523953afc39 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 4 Feb 2021 13:07:16 +0300 Subject: Adds Move To Failure Tests Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 2d85af7e0..70fe756fd 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -224,6 +224,32 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): else: self.assertEqual((None,), member.move_to.call_args_list[0].args) + async def test_voice_move_to_error(self): + """Test to ensure move_to get called on all members, even if some fail.""" + await self.cog._async_init() + + def failing_move_to(*_): + raise Exception() + failing_members = [MockMember(move_to=Mock(failing_move_to)) for _ in range(5)] + + members = [] + for i in range(5): + members.append(MockMember()) + members.append(failing_members[i]) + + channel = MockVoiceChannel(members=members) + + with self.subTest("Kick"): + await self.cog._kick_voice_members(channel) + for member in members: + member.move_to.assert_called_once() + member.reset_mock() + + with self.subTest("Sync"): + await self.cog._force_voice_sync(channel) + for member in members: + self.assertEqual(member.move_to.call_count, 1 if member in failing_members else 2) + @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) class RescheduleTests(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From 09886c234c9840dc2c2eca0f0a26e72ae6cee527 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 4 Feb 2021 19:58:50 +0300 Subject: Separates Voice Overwrite Tests Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 70fe756fd..6b48792cb 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -461,7 +461,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.assertFalse(await self.cog._set_silence_overwrites(channel)) channel.set_permissions.assert_not_called() - async def test_silenced_channel(self): + async def test_silenced_text_channel(self): """Channel had `send_message` and `add_reactions` permissions revoked for verified role.""" self.assertTrue(await self.cog._set_silence_overwrites(self.text_channel)) self.assertFalse(self.text_overwrite.send_messages) @@ -471,6 +471,24 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): overwrite=self.text_overwrite ) + async def test_silenced_voice_channel_speak(self): + """Channel had `speak` permissions revoked for verified role.""" + self.assertTrue(await self.cog._set_silence_overwrites(self.voice_channel)) + self.assertFalse(self.voice_overwrite.speak) + self.voice_channel.set_permissions.assert_awaited_once_with( + self.cog._verified_voice_role, + overwrite=self.voice_overwrite + ) + + async def test_silenced_voice_channel_full(self): + """Channel had `speak` and `connect` permissions revoked for verified role.""" + self.assertTrue(await self.cog._set_silence_overwrites(self.voice_channel, kick=True)) + self.assertFalse(self.voice_overwrite.speak or self.voice_overwrite.connect) + self.voice_channel.set_permissions.assert_awaited_once_with( + self.cog._verified_voice_role, + overwrite=self.voice_overwrite + ) + async def test_preserved_other_overwrites(self): """Channel's other unrelated overwrites were not changed.""" prev_overwrite_dict = dict(self.text_overwrite) @@ -545,14 +563,6 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog.silence.callback(self.cog, ctx, None) self.cog.scheduler.schedule_later.assert_not_called() - async def test_correct_permission_updates(self): - """Tests if _set_silence_overwrites can correctly get and update permissions.""" - self.assertTrue(await self.cog._set_silence_overwrites(self.voice_channel)) - self.assertFalse(self.voice_overwrite.speak) - - self.assertTrue(await self.cog._set_silence_overwrites(self.voice_channel, kick=True)) - self.assertFalse(self.voice_overwrite.speak or self.voice_overwrite.connect) - @autospec(silence.Silence, "unsilence_timestamps", pass_mocks=False) class UnsilenceTests(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From 4fea67cb8a535228cb37a83c4a2d44b5112fb707 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Fri, 5 Feb 2021 02:23:10 +0300 Subject: Modifies Silence Tests Adds a missing test assertion, and seperates the voice and text components of a test. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 6b48792cb..2fcf4de43 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -648,14 +648,15 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.assertIsNone(self.overwrite.send_messages) self.assertIsNone(self.overwrite.add_reactions) - async def test_cache_miss_sent_mod_alert(self): - """A message was sent to the mod alerts channel.""" + async def test_cache_miss_sent_mod_alert_text(self): + """A message was sent to the mod alerts channel upon muting a text channel.""" self.cog.previous_overwrites.get.return_value = None - await self.cog._unsilence(self.channel) self.cog._mod_alerts_channel.send.assert_awaited_once() - self.cog._mod_alerts_channel.send.reset_mock() + async def test_cache_miss_sent_mod_alert_voice(self): + """A message was sent to the mod alerts channel upon muting a voice channel.""" + self.cog.previous_overwrites.get.return_value = None await self.cog._unsilence(MockVoiceChannel()) self.cog._mod_alerts_channel.send.assert_awaited_once() @@ -832,6 +833,7 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): message = "This should show up just here." await self.cog.send_message(message, self.text_channels[0], self.voice_channel, alert_target=False) self.text_channels[0].send.assert_called_once_with(message) + self.text_channels[1].send.assert_not_called() async def test_silence_voice_alert(self): """Tests that the correct message was sent when a voice channel is muted with alerts.""" -- cgit v1.2.3 From 1343a1c22bcd8a21c2d3fc38293033f546c86036 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Fri, 5 Feb 2021 02:49:06 +0300 Subject: Modifies Silence Tests Adds a missing test assertion, and seperates the voice and text components of a test. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 43 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 2fcf4de43..a52f2447d 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -1,6 +1,7 @@ import asyncio import unittest from datetime import datetime, timezone +from typing import List, Tuple from unittest import mock from unittest.mock import AsyncMock, Mock @@ -224,31 +225,43 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): else: self.assertEqual((None,), member.move_to.call_args_list[0].args) - async def test_voice_move_to_error(self): - """Test to ensure move_to get called on all members, even if some fail.""" - await self.cog._async_init() + @staticmethod + def create_erroneous_members() -> Tuple[List[MockMember], List[MockMember]]: + """ + Helper method to generate a list of members that error out on move_to call. + Returns the list of erroneous members, + as well as a list of regular and erroneous members combined, in that order. + """ def failing_move_to(*_): raise Exception() - failing_members = [MockMember(move_to=Mock(failing_move_to)) for _ in range(5)] + erroneous_members = [MockMember(move_to=Mock(failing_move_to)) for _ in range(5)] members = [] for i in range(5): members.append(MockMember()) - members.append(failing_members[i]) + members.append(erroneous_members[i]) - channel = MockVoiceChannel(members=members) + return erroneous_members, members + + async def test_kick_move_to_error(self): + """Test to ensure move_to gets called on all members during kick, even if some fail.""" + await self.cog._async_init() + failing_members, members = self.create_erroneous_members() - with self.subTest("Kick"): - await self.cog._kick_voice_members(channel) - for member in members: - member.move_to.assert_called_once() - member.reset_mock() + await self.cog._kick_voice_members(MockVoiceChannel(members=members)) + for member in members: + member.move_to.assert_called_once() + member.reset_mock() + + async def test_sync_move_to_error(self): + """Test to ensure move_to gets called on all members during sync, even if some fail.""" + await self.cog._async_init() + failing_members, members = self.create_erroneous_members() - with self.subTest("Sync"): - await self.cog._force_voice_sync(channel) - for member in members: - self.assertEqual(member.move_to.call_count, 1 if member in failing_members else 2) + await self.cog._force_voice_sync(MockVoiceChannel(members=members)) + for member in members: + self.assertEqual(member.move_to.call_count, 1 if member in failing_members else 2) @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) -- cgit v1.2.3 From acf2b643672703d2bc2011558e9fb68c76d0bc17 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Wed, 10 Mar 2021 20:36:36 +0300 Subject: Combine Silence Target Tests Combine two tests that are responsible for checking the silence helper uses the correct channel and message. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/moderation/silence.py | 2 +- tests/bot/exts/moderation/test_silence.py | 53 +++++++++---------------------- 2 files changed, 16 insertions(+), 39 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index dd379b412..616dfbefb 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -173,7 +173,7 @@ class Silence(commands.Cog): if not await self._set_silence_overwrites(channel, kick=kick): log.info(f"Tried to silence channel {channel_info} but the channel was already silenced.") - await self.send_message(MSG_SILENCE_FAIL, ctx.channel, channel) + await self.send_message(MSG_SILENCE_FAIL, ctx.channel, channel, alert_target=False) return if isinstance(channel, VoiceChannel): diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index a52f2447d..c3b30450f 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -375,51 +375,28 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.voice_channel.overwrites_for.return_value = self.voice_overwrite async def test_sent_correct_message(self): - """Appropriate failure/success message was sent by the command.""" + """Appropriate failure/success message was sent by the command to the correct channel.""" + # The following test tuples are made up of: + # duration, expected message, and the success of the _set_silence_overwrites function test_cases = ( (0.0001, silence.MSG_SILENCE_SUCCESS.format(duration=0.0001), True,), (None, silence.MSG_SILENCE_PERMANENT, True,), (5, silence.MSG_SILENCE_FAIL, False,), ) + for duration, message, was_silenced in test_cases: - ctx = MockContext() with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=was_silenced): - with self.subTest(was_silenced=was_silenced, message=message, duration=duration): - await self.cog.silence.callback(self.cog, ctx, duration) - ctx.channel.send.assert_called_once_with(message) - - @mock.patch.object(silence.Silence, "_set_silence_overwrites", return_value=True) - @mock.patch.object(silence.Silence, "_force_voice_sync") - async def test_sent_to_correct_channel(self, voice_sync, _): - """Test function sends messages to the correct channels.""" - text_channel = MockTextChannel() - voice_channel = MockVoiceChannel() - ctx = MockContext() - - test_cases = ( - (None, silence.MSG_SILENCE_SUCCESS.format(duration=10)), - (text_channel, silence.MSG_SILENCE_SUCCESS.format(duration=10)), - (voice_channel, silence.MSG_SILENCE_SUCCESS.format(duration=10)), - (ctx.channel, silence.MSG_SILENCE_SUCCESS.format(duration=10)), - ) - - for target, message in test_cases: - with self.subTest(target_channel=target, message=message): - await self.cog.silence.callback(self.cog, ctx, 10, target, kick=False) - - if ctx.channel == target or target is None: - ctx.channel.send.assert_called_once_with(message) - - else: - ctx.channel.send.assert_called_once_with(message.replace("current channel", target.mention)) - if isinstance(target, MockTextChannel): - target.send.assert_called_once_with(message) - else: - voice_sync.assert_called_once_with(target) - - ctx.channel.send.reset_mock() - if target is not None and isinstance(target, MockTextChannel): - target.send.reset_mock() + for target in [MockTextChannel(), MockVoiceChannel(), None]: + with self.subTest(was_silenced=was_silenced, target=target, message=message): + with mock.patch.object(self.cog, "send_message") as send_message: + ctx = MockContext() + await self.cog.silence.callback(self.cog, ctx, duration, target) + send_message.assert_called_once_with( + message, + ctx.channel, + target or ctx.channel, + alert_target=was_silenced + ) @voice_sync_helper async def test_sync_called(self, ctx, sync, kick): -- cgit v1.2.3 From e62707dfaf2dd833ff057ee472472d27a86ac223 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Wed, 10 Mar 2021 20:55:24 +0300 Subject: Simplifies Redundant Unsilence Target Test Removes redundant functionality from the `test_unsilence_helper_fail` test as it is covered by another test. Keeps the functionality that isn't being tested elsewhere. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 41 ++++++------------------------- 1 file changed, 8 insertions(+), 33 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index c3b30450f..5f2e67ac2 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -688,42 +688,17 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) - @mock.patch.object(silence.Silence, "_unsilence", return_value=False) - @mock.patch.object(silence.Silence, "send_message") - async def test_unsilence_helper_fail(self, send_message, _): - """Tests unsilence_wrapper when `_unsilence` fails.""" - ctx = MockContext() - - text_channel = MockTextChannel() - text_role = self.cog.bot.get_guild(Guild.id).default_role - - voice_channel = MockVoiceChannel() - voice_role = self.cog.bot.get_guild(Guild.id).get_role(Roles.voice_verified) - + async def test_unsilence_role(self): + """Tests unsilence_wrapper applies permission to the correct role.""" test_cases = ( - (ctx, text_channel, text_role, True, silence.MSG_UNSILENCE_FAIL), - (ctx, text_channel, text_role, False, silence.MSG_UNSILENCE_MANUAL), - (ctx, voice_channel, voice_role, True, silence.MSG_UNSILENCE_FAIL), - (ctx, voice_channel, voice_role, False, silence.MSG_UNSILENCE_MANUAL), + (MockTextChannel(), self.cog.bot.get_guild(Guild.id).default_role), + (MockVoiceChannel(), self.cog.bot.get_guild(Guild.id).get_role(Roles.voice_verified)) ) - class PermClass: - """Class to Mock return permissions""" - def __init__(self, value: bool): - self.val = value - - def __getattr__(self, item): - return self.val - - for context, channel, role, permission, message in test_cases: - with mock.patch.object(channel, "overwrites_for", return_value=PermClass(permission)) as overwrites: - with self.subTest(channel=channel, message=message): - await self.cog._unsilence_wrapper(channel, context) - - overwrites.assert_called_once_with(role) - send_message.assert_called_once_with(message, ctx.channel, channel, alert_target=False) - - send_message.reset_mock() + for channel, role in test_cases: + with self.subTest(channel=channel, role=role): + await self.cog._unsilence_wrapper(channel, MockContext()) + channel.overwrites_for.assert_called_with(role) @mock.patch.object(silence.Silence, "_force_voice_sync") @mock.patch.object(silence.Silence, "send_message") -- cgit v1.2.3 From b867fb21b6a14f91aa608009aa7eef540a4792dc Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Wed, 10 Mar 2021 22:38:59 +0300 Subject: Use Mock Side Effect Instead Of Extra Function Changes the mock used for creating an erroneous function in the silence tests cog to use the side effect property instead of an extra function. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 5f2e67ac2..a297cc8cb 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -233,9 +233,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): Returns the list of erroneous members, as well as a list of regular and erroneous members combined, in that order. """ - def failing_move_to(*_): - raise Exception() - erroneous_members = [MockMember(move_to=Mock(failing_move_to)) for _ in range(5)] + erroneous_members = [MockMember(move_to=Mock(side_effect=Exception())) for _ in range(5)] members = [] for i in range(5): -- cgit v1.2.3 From b0381d7164ceba20784fc244d724962ec35a9b13 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Fri, 12 Mar 2021 22:58:28 +0300 Subject: Removes Unused Mock Reset Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- tests/bot/exts/moderation/test_silence.py | 1 - 1 file changed, 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index a297cc8cb..d7542c562 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -250,7 +250,6 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): await self.cog._kick_voice_members(MockVoiceChannel(members=members)) for member in members: member.move_to.assert_called_once() - member.reset_mock() async def test_sync_move_to_error(self): """Test to ensure move_to gets called on all members during sync, even if some fail.""" -- cgit v1.2.3 From 2446c1728c3ba660d33b686d93d62a93debc74d2 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Wed, 21 Apr 2021 00:25:51 +0300 Subject: Removes Unnecessary Members In Silence Tests Reduces the number of members created for each test to the bare minimum required. Signed-off-by: Hassan Abouelela --- tests/bot/exts/moderation/test_silence.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index d7542c562..459048f68 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -173,14 +173,15 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): """Tests the _force_voice_sync helper function.""" await self.cog._async_init() - members = [MockMember() for _ in range(10)] - members.extend([MockMember(roles=[MockRole(id=role)]) for role in MODERATION_ROLES]) + # Create a regular member, and one member for each of the moderation roles + moderation_members = [MockMember(roles=[MockRole(id=role)]) for role in MODERATION_ROLES] + members = [MockMember(), *moderation_members] channel = MockVoiceChannel(members=members) await self.cog._force_voice_sync(channel) for member in members: - if any(role.id in MODERATION_ROLES for role in member.roles): + if member in moderation_members: member.move_to.assert_not_called() else: self.assertEqual(member.move_to.call_count, 2) @@ -213,14 +214,15 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): """Test to ensure kick function can remove all members from a voice channel.""" await self.cog._async_init() - members = [MockMember() for _ in range(10)] - members.extend([MockMember(roles=[MockRole(id=role)]) for role in MODERATION_ROLES]) + # Create a regular member, and one member for each of the moderation roles + moderation_members = [MockMember(roles=[MockRole(id=role)]) for role in MODERATION_ROLES] + members = [MockMember(), *moderation_members] channel = MockVoiceChannel(members=members) await self.cog._kick_voice_members(channel) for member in members: - if any(role.id in MODERATION_ROLES for role in member.roles): + if member in moderation_members: member.move_to.assert_not_called() else: self.assertEqual((None,), member.move_to.call_args_list[0].args) @@ -233,19 +235,15 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): Returns the list of erroneous members, as well as a list of regular and erroneous members combined, in that order. """ - erroneous_members = [MockMember(move_to=Mock(side_effect=Exception())) for _ in range(5)] + erroneous_member = MockMember(move_to=Mock(side_effect=Exception())) + members = [MockMember(), erroneous_member] - members = [] - for i in range(5): - members.append(MockMember()) - members.append(erroneous_members[i]) - - return erroneous_members, members + return erroneous_member, members async def test_kick_move_to_error(self): """Test to ensure move_to gets called on all members during kick, even if some fail.""" await self.cog._async_init() - failing_members, members = self.create_erroneous_members() + _, members = self.create_erroneous_members() await self.cog._kick_voice_members(MockVoiceChannel(members=members)) for member in members: @@ -254,11 +252,11 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): async def test_sync_move_to_error(self): """Test to ensure move_to gets called on all members during sync, even if some fail.""" await self.cog._async_init() - failing_members, members = self.create_erroneous_members() + failing_member, members = self.create_erroneous_members() await self.cog._force_voice_sync(MockVoiceChannel(members=members)) for member in members: - self.assertEqual(member.move_to.call_count, 1 if member in failing_members else 2) + self.assertEqual(member.move_to.call_count, 1 if member == failing_member else 2) @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) -- cgit v1.2.3 From da694fde0f78813a2787f371b8da84c39b72bcd9 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 24 Apr 2021 01:10:13 +0300 Subject: Uses Async Asserts Where Possible Signed-off-by: Hassan Abouelela --- tests/bot/exts/moderation/test_silence.py | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 459048f68..ce76dc945 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -205,10 +205,10 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): overwrites = { channel.guild.default_role: PermissionOverwrite(speak=False, connect=False, view_channel=False) } - channel.guild.create_voice_channel.assert_called_once_with("mute-temp", overwrites=overwrites) + channel.guild.create_voice_channel.assert_awaited_once_with("mute-temp", overwrites=overwrites) # Check bot deleted channel - new_channel.delete.assert_called_once() + new_channel.delete.assert_awaited_once() async def test_voice_kick(self): """Test to ensure kick function can remove all members from a voice channel.""" @@ -235,7 +235,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): Returns the list of erroneous members, as well as a list of regular and erroneous members combined, in that order. """ - erroneous_member = MockMember(move_to=Mock(side_effect=Exception())) + erroneous_member = MockMember(move_to=AsyncMock(side_effect=Exception())) members = [MockMember(), erroneous_member] return erroneous_member, members @@ -247,7 +247,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): await self.cog._kick_voice_members(MockVoiceChannel(members=members)) for member in members: - member.move_to.assert_called_once() + member.move_to.assert_awaited_once() async def test_sync_move_to_error(self): """Test to ensure move_to gets called on all members during sync, even if some fail.""" @@ -399,7 +399,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): channel = MockVoiceChannel() await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=False) - sync.assert_called_once_with(self.cog, channel) + sync.assert_awaited_once_with(self.cog, channel) kick.assert_not_called() @voice_sync_helper @@ -408,7 +408,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): channel = MockVoiceChannel() await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=True) - kick.assert_called_once_with(channel) + kick.assert_awaited_once_with(channel) sync.assert_not_called() @voice_sync_helper @@ -510,7 +510,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): """Channel's previous overwrites were cached.""" overwrite_json = '{"send_messages": true, "add_reactions": false}' await self.cog._set_silence_overwrites(self.text_channel) - self.cog.previous_overwrites.set.assert_called_once_with(self.text_channel.id, overwrite_json) + self.cog.previous_overwrites.set.assert_awaited_once_with(self.text_channel.id, overwrite_json) @autospec(silence, "datetime") async def test_cached_unsilence_time(self, datetime_mock): @@ -597,7 +597,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog.unsilence.callback(self.cog, ctx, channel=target) call_args = (message, ctx.channel, target or ctx.channel) - send_message.assert_called_once_with(*call_args, alert_target=was_unsilenced) + send_message.assert_awaited_once_with(*call_args, alert_target=was_unsilenced) async def test_skipped_already_unsilenced(self): """Permissions were not set and `False` was returned for an already unsilenced channel.""" @@ -731,11 +731,11 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._unsilence_wrapper(channel, context) if context is None: - send_message.assert_called_once_with(message, channel, channel, alert_target=True) + send_message.assert_awaited_once_with(message, channel, channel, alert_target=True) else: - send_message.assert_called_once_with(message, context.channel, channel, alert_target=True) + send_message.assert_awaited_once_with(message, context.channel, channel, alert_target=True) - channel.set_permissions.assert_called_once_with(role, overwrite=overwrites) + channel.set_permissions.assert_awaited_once_with(role, overwrite=overwrites) if channel != ctx.channel: ctx.channel.send.assert_not_called() @@ -759,7 +759,7 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): message = "Test basic message." await self.cog.send_message(message, *self.text_channels, alert_target=False) - self.text_channels[0].send.assert_called_once_with(message) + self.text_channels[0].send.assert_awaited_once_with(message) self.text_channels[1].send.assert_not_called() async def test_send_to_multiple_channels(self): @@ -767,8 +767,8 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): message = "Test basic message." await self.cog.send_message(message, *self.text_channels, alert_target=True) - self.text_channels[0].send.assert_called_once_with(message) - self.text_channels[1].send.assert_called_once_with(message) + self.text_channels[0].send.assert_awaited_once_with(message) + self.text_channels[1].send.assert_awaited_once_with(message) async def test_duration_replacement(self): """Tests that the channel name was set correctly for one target channel.""" @@ -776,7 +776,7 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): await self.cog.send_message(message, *self.text_channels, alert_target=False) updated_message = message.replace("current channel", self.text_channels[0].mention) - self.text_channels[0].send.assert_called_once_with(updated_message) + self.text_channels[0].send.assert_awaited_once_with(updated_message) self.text_channels[1].send.assert_not_called() async def test_name_replacement_multiple_channels(self): @@ -785,14 +785,14 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): await self.cog.send_message(message, *self.text_channels, alert_target=True) updated_message = message.replace("current channel", self.text_channels[0].mention) - self.text_channels[0].send.assert_called_once_with(updated_message) - self.text_channels[1].send.assert_called_once_with(message) + self.text_channels[0].send.assert_awaited_once_with(updated_message) + self.text_channels[1].send.assert_awaited_once_with(message) async def test_silence_voice(self): """Tests that the correct message was sent when a voice channel is muted without alerting.""" message = "This should show up just here." await self.cog.send_message(message, self.text_channels[0], self.voice_channel, alert_target=False) - self.text_channels[0].send.assert_called_once_with(message) + self.text_channels[0].send.assert_awaited_once_with(message) self.text_channels[1].send.assert_not_called() async def test_silence_voice_alert(self): @@ -804,8 +804,8 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): await self.cog.send_message(message, self.text_channels[0], self.voice_channel, alert_target=True) updated_message = message.replace("current channel", self.voice_channel.mention) - self.text_channels[0].send.assert_called_once_with(updated_message) - self.text_channels[1].send.assert_called_once_with(updated_message) + self.text_channels[0].send.assert_awaited_once_with(updated_message) + self.text_channels[1].send.assert_awaited_once_with(updated_message) mock_voice_channels.get.assert_called_once_with(self.voice_channel.id) @@ -818,7 +818,7 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): await self.cog.send_message(message, self.text_channels[1], self.voice_channel, alert_target=True) updated_message = message.replace("current channel", self.voice_channel.mention) - self.text_channels[1].send.assert_called_once_with(updated_message) + self.text_channels[1].send.assert_awaited_once_with(updated_message) mock_voice_channels.get.assert_called_once_with(self.voice_channel.id) self.bot.get_channel.assert_called_once_with(self.text_channels[1].id) -- cgit v1.2.3 From db5b0751fb9531525e12e6d421aa7d0772b3054a Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Tue, 4 May 2021 04:37:49 +0300 Subject: Copy Existing Text Channel Cache Tests For Voice Duplicates existing silence and unsilence cache tests for voice channels. Signed-off-by: Hassan Abouelela --- tests/bot/exts/moderation/test_silence.py | 179 ++++++++++++++++-------------- 1 file changed, 97 insertions(+), 82 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index ce76dc945..347471c13 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -362,11 +362,11 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): asyncio.run(self.cog._async_init()) # Populate instance attributes. self.text_channel = MockTextChannel() - self.text_overwrite = PermissionOverwrite(stream=True, send_messages=True, add_reactions=False) + self.text_overwrite = PermissionOverwrite(send_messages=True, add_reactions=False) self.text_channel.overwrites_for.return_value = self.text_overwrite self.voice_channel = MockVoiceChannel() - self.voice_overwrite = PermissionOverwrite(speak=True) + self.voice_overwrite = PermissionOverwrite(connect=True, speak=True) self.voice_channel.overwrites_for.return_value = self.voice_overwrite async def test_sent_correct_message(self): @@ -474,8 +474,8 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): overwrite=self.voice_overwrite ) - async def test_preserved_other_overwrites(self): - """Channel's other unrelated overwrites were not changed.""" + async def test_preserved_other_overwrites_text(self): + """Channel's other unrelated overwrites were not changed for a text channel mute.""" prev_overwrite_dict = dict(self.text_overwrite) await self.cog._set_silence_overwrites(self.text_channel) new_overwrite_dict = dict(self.text_overwrite) @@ -488,6 +488,20 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) + async def test_preserved_other_overwrites_voice(self): + """Channel's other unrelated overwrites were not changed for a voice channel mute.""" + prev_overwrite_dict = dict(self.voice_overwrite) + await self.cog._set_silence_overwrites(self.voice_channel) + new_overwrite_dict = dict(self.voice_overwrite) + + # Remove 'connect' & 'speak' keys because they were changed by the method. + del prev_overwrite_dict['connect'] + del prev_overwrite_dict['speak'] + del new_overwrite_dict['connect'] + del new_overwrite_dict['speak'] + + self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) + async def test_temp_not_added_to_notifier(self): """Channel was not added to notifier if a duration was set for the silence.""" with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=True): @@ -568,9 +582,13 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.cog.scheduler.__contains__.return_value = True overwrites_cache.get.return_value = '{"send_messages": true, "add_reactions": false}' - self.channel = MockTextChannel() - self.overwrite = PermissionOverwrite(stream=True, send_messages=False, add_reactions=False) - self.channel.overwrites_for.return_value = self.overwrite + self.text_channel = MockTextChannel() + self.text_overwrite = PermissionOverwrite(send_messages=False, add_reactions=False) + self.text_channel.overwrites_for.return_value = self.text_overwrite + + self.voice_channel = MockVoiceChannel() + self.voice_overwrite = PermissionOverwrite(connect=True, speak=True) + self.voice_channel.overwrites_for.return_value = self.voice_overwrite async def test_sent_correct_message(self): """Appropriate failure/success message was sent by the command.""" @@ -578,7 +596,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): test_cases = ( (True, silence.MSG_UNSILENCE_SUCCESS, unsilenced_overwrite), (False, silence.MSG_UNSILENCE_FAIL, unsilenced_overwrite), - (False, silence.MSG_UNSILENCE_MANUAL, self.overwrite), + (False, silence.MSG_UNSILENCE_MANUAL, self.text_overwrite), (False, silence.MSG_UNSILENCE_MANUAL, PermissionOverwrite(send_messages=False)), (False, silence.MSG_UNSILENCE_MANUAL, PermissionOverwrite(add_reactions=False)), ) @@ -608,35 +626,60 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.assertFalse(await self.cog._unsilence(channel)) channel.set_permissions.assert_not_called() - async def test_restored_overwrites(self): - """Channel's `send_message` and `add_reactions` overwrites were restored.""" - await self.cog._unsilence(self.channel) - self.channel.set_permissions.assert_awaited_once_with( + async def test_restored_overwrites_text(self): + """Text channel's `send_message` and `add_reactions` overwrites were restored.""" + await self.cog._unsilence(self.text_channel) + self.text_channel.set_permissions.assert_awaited_once_with( self.cog._everyone_role, - overwrite=self.overwrite, + overwrite=self.text_overwrite, + ) + + # Recall that these values are determined by the fixture. + self.assertTrue(self.text_overwrite.send_messages) + self.assertFalse(self.text_overwrite.add_reactions) + + async def test_restored_overwrites_voice(self): + """Voice channel's `connect` and `speak` overwrites were restored.""" + await self.cog._unsilence(self.voice_channel) + self.voice_channel.set_permissions.assert_awaited_once_with( + self.cog._verified_voice_role, + overwrite=self.voice_overwrite, ) # Recall that these values are determined by the fixture. - self.assertTrue(self.overwrite.send_messages) - self.assertFalse(self.overwrite.add_reactions) + self.assertTrue(self.voice_overwrite.connect) + self.assertTrue(self.voice_overwrite.speak) - async def test_cache_miss_used_default_overwrites(self): - """Both overwrites were set to None due previous values not being found in the cache.""" + async def test_cache_miss_used_default_overwrites_text(self): + """Text overwrites were set to None due previous values not being found in the cache.""" self.cog.previous_overwrites.get.return_value = None - await self.cog._unsilence(self.channel) - self.channel.set_permissions.assert_awaited_once_with( + await self.cog._unsilence(self.text_channel) + self.text_channel.set_permissions.assert_awaited_once_with( self.cog._everyone_role, - overwrite=self.overwrite, + overwrite=self.text_overwrite, ) - self.assertIsNone(self.overwrite.send_messages) - self.assertIsNone(self.overwrite.add_reactions) + self.assertIsNone(self.text_overwrite.send_messages) + self.assertIsNone(self.text_overwrite.add_reactions) + + async def test_cache_miss_used_default_overwrites_voice(self): + """Voice overwrites were set to None due previous values not being found in the cache.""" + self.cog.previous_overwrites.get.return_value = None + + await self.cog._unsilence(self.voice_channel) + self.voice_channel.set_permissions.assert_awaited_once_with( + self.cog._verified_voice_role, + overwrite=self.voice_overwrite, + ) + + self.assertIsNone(self.voice_overwrite.connect) + self.assertIsNone(self.voice_overwrite.speak) async def test_cache_miss_sent_mod_alert_text(self): """A message was sent to the mod alerts channel upon muting a text channel.""" self.cog.previous_overwrites.get.return_value = None - await self.cog._unsilence(self.channel) + await self.cog._unsilence(self.text_channel) self.cog._mod_alerts_channel.send.assert_awaited_once() async def test_cache_miss_sent_mod_alert_voice(self): @@ -647,33 +690,33 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): async def test_removed_notifier(self): """Channel was removed from `notifier`.""" - await self.cog._unsilence(self.channel) - self.cog.notifier.remove_channel.assert_called_once_with(self.channel) + await self.cog._unsilence(self.text_channel) + self.cog.notifier.remove_channel.assert_called_once_with(self.text_channel) async def test_deleted_cached_overwrite(self): """Channel was deleted from the overwrites cache.""" - await self.cog._unsilence(self.channel) - self.cog.previous_overwrites.delete.assert_awaited_once_with(self.channel.id) + await self.cog._unsilence(self.text_channel) + self.cog.previous_overwrites.delete.assert_awaited_once_with(self.text_channel.id) async def test_deleted_cached_time(self): """Channel was deleted from the timestamp cache.""" - await self.cog._unsilence(self.channel) - self.cog.unsilence_timestamps.delete.assert_awaited_once_with(self.channel.id) + await self.cog._unsilence(self.text_channel) + self.cog.unsilence_timestamps.delete.assert_awaited_once_with(self.text_channel.id) async def test_cancelled_task(self): """The scheduled unsilence task should be cancelled.""" - await self.cog._unsilence(self.channel) - self.cog.scheduler.cancel.assert_called_once_with(self.channel.id) + await self.cog._unsilence(self.text_channel) + self.cog.scheduler.cancel.assert_called_once_with(self.text_channel.id) - async def test_preserved_other_overwrites(self): - """Channel's other unrelated overwrites were not changed, including cache misses.""" + async def test_preserved_other_overwrites_text(self): + """Text channel's other unrelated overwrites were not changed, including cache misses.""" for overwrite_json in ('{"send_messages": true, "add_reactions": null}', None): with self.subTest(overwrite_json=overwrite_json): self.cog.previous_overwrites.get.return_value = overwrite_json - prev_overwrite_dict = dict(self.overwrite) - await self.cog._unsilence(self.channel) - new_overwrite_dict = dict(self.overwrite) + prev_overwrite_dict = dict(self.text_overwrite) + await self.cog._unsilence(self.text_channel) + new_overwrite_dict = dict(self.text_overwrite) # Remove these keys because they were modified by the unsilence. del prev_overwrite_dict['send_messages'] @@ -683,6 +726,24 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) + async def test_preserved_other_overwrites_voice(self): + """Voice channel's other unrelated overwrites were not changed, including cache misses.""" + for overwrite_json in ('{"connect": true, "speak": true}', None): + with self.subTest(overwrite_json=overwrite_json): + self.cog.previous_overwrites.get.return_value = overwrite_json + + prev_overwrite_dict = dict(self.voice_overwrite) + await self.cog._unsilence(self.voice_channel) + new_overwrite_dict = dict(self.voice_overwrite) + + # Remove these keys because they were modified by the unsilence. + del prev_overwrite_dict['connect'] + del prev_overwrite_dict['speak'] + del new_overwrite_dict['connect'] + del new_overwrite_dict['speak'] + + self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) + async def test_unsilence_role(self): """Tests unsilence_wrapper applies permission to the correct role.""" test_cases = ( @@ -695,52 +756,6 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._unsilence_wrapper(channel, MockContext()) channel.overwrites_for.assert_called_with(role) - @mock.patch.object(silence.Silence, "_force_voice_sync") - @mock.patch.object(silence.Silence, "send_message") - async def test_correct_overwrites(self, send_message, _): - """Tests the overwrites returned by the _unsilence_wrapper are correct for voice and text channels.""" - ctx = MockContext() - - text_channel = MockTextChannel() - text_role = self.cog.bot.get_guild(Guild.id).default_role - - voice_channel = MockVoiceChannel() - voice_role = self.cog.bot.get_guild(Guild.id).get_role(Roles.voice_verified) - - async def reset(): - await text_channel.set_permissions(text_role, PermissionOverwrite(send_messages=False, add_reactions=False)) - await voice_channel.set_permissions(voice_role, PermissionOverwrite(speak=False, connect=False)) - - text_channel.reset_mock() - voice_channel.reset_mock() - send_message.reset_mock() - await reset() - - default_text_overwrites = text_channel.overwrites_for(text_role) - default_voice_overwrites = voice_channel.overwrites_for(voice_role) - - test_cases = ( - (ctx, text_channel, text_role, default_text_overwrites, silence.MSG_UNSILENCE_SUCCESS), - (ctx, voice_channel, voice_role, default_voice_overwrites, silence.MSG_UNSILENCE_SUCCESS), - (ctx, ctx.channel, text_role, ctx.channel.overwrites_for(text_role), silence.MSG_UNSILENCE_SUCCESS), - (None, text_channel, text_role, default_text_overwrites, silence.MSG_UNSILENCE_SUCCESS), - ) - - for context, channel, role, overwrites, message in test_cases: - with self.subTest(ctx=context, channel=channel): - await self.cog._unsilence_wrapper(channel, context) - - if context is None: - send_message.assert_awaited_once_with(message, channel, channel, alert_target=True) - else: - send_message.assert_awaited_once_with(message, context.channel, channel, alert_target=True) - - channel.set_permissions.assert_awaited_once_with(role, overwrite=overwrites) - if channel != ctx.channel: - ctx.channel.send.assert_not_called() - - await reset() - class SendMessageTests(unittest.IsolatedAsyncioTestCase): """Unittests for the send message helper function.""" -- cgit v1.2.3 From bac68d1d584b398f2bc5cc7a8f3df39ed48174ae Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Tue, 4 May 2021 04:47:15 +0300 Subject: Adds Voice Test Cases To Already Silenced Test Signed-off-by: Hassan Abouelela --- tests/bot/exts/moderation/test_silence.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 347471c13..0d135698e 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -432,15 +432,17 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_skipped_already_silenced(self): """Permissions were not set and `False` was returned for an already silenced channel.""" subtests = ( - (False, PermissionOverwrite(send_messages=False, add_reactions=False)), - (True, PermissionOverwrite(send_messages=True, add_reactions=True)), - (True, PermissionOverwrite(send_messages=False, add_reactions=False)), + (False, MockTextChannel(), PermissionOverwrite(send_messages=False, add_reactions=False)), + (True, MockTextChannel(), PermissionOverwrite(send_messages=True, add_reactions=True)), + (True, MockTextChannel(), PermissionOverwrite(send_messages=False, add_reactions=False)), + (False, MockVoiceChannel(), PermissionOverwrite(connect=False, speak=False)), + (True, MockVoiceChannel(), PermissionOverwrite(connect=True, speak=True)), + (True, MockVoiceChannel(), PermissionOverwrite(connect=False, speak=False)), ) - for contains, overwrite in subtests: - with self.subTest(contains=contains, overwrite=overwrite): + for contains, channel, overwrite in subtests: + with self.subTest(contains=contains, is_text=isinstance(channel, MockTextChannel), overwrite=overwrite): self.cog.scheduler.__contains__.return_value = contains - channel = MockTextChannel() channel.overwrites_for.return_value = overwrite self.assertFalse(await self.cog._set_silence_overwrites(channel)) -- cgit v1.2.3 From 4bf9a7a545e2ffb507fbb379df1695755a2eea1b Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Tue, 4 May 2021 05:09:04 +0300 Subject: Adds Missing Voice Version Of Tests Signed-off-by: Hassan Abouelela --- tests/bot/exts/moderation/test_silence.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 0d135698e..729b28412 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -623,10 +623,11 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): """Permissions were not set and `False` was returned for an already unsilenced channel.""" self.cog.scheduler.__contains__.return_value = False self.cog.previous_overwrites.get.return_value = None - channel = MockTextChannel() - self.assertFalse(await self.cog._unsilence(channel)) - channel.set_permissions.assert_not_called() + for channel in (MockVoiceChannel(), MockTextChannel()): + with self.subTest(channel=channel): + self.assertFalse(await self.cog._unsilence(channel)) + channel.set_permissions.assert_not_called() async def test_restored_overwrites_text(self): """Text channel's `send_message` and `add_reactions` overwrites were restored.""" -- cgit v1.2.3 From 2722352236bcad411bd6417e7009e43625a7da9a Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sun, 9 May 2021 00:32:13 +0300 Subject: Uses Itertools Product To Reduce Nesting Uses itertools.product to eliminate some nested for loops in tests. Signed-off-by: Hassan Abouelela --- tests/bot/exts/moderation/test_silence.py | 50 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 24 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 729b28412..de7230ae5 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -1,4 +1,5 @@ import asyncio +import itertools import unittest from datetime import datetime, timezone from typing import List, Tuple @@ -379,19 +380,20 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): (5, silence.MSG_SILENCE_FAIL, False,), ) - for duration, message, was_silenced in test_cases: + targets = (MockTextChannel(), MockVoiceChannel(), None) + + for (duration, message, was_silenced), target in itertools.product(test_cases, targets): with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=was_silenced): - for target in [MockTextChannel(), MockVoiceChannel(), None]: - with self.subTest(was_silenced=was_silenced, target=target, message=message): - with mock.patch.object(self.cog, "send_message") as send_message: - ctx = MockContext() - await self.cog.silence.callback(self.cog, ctx, duration, target) - send_message.assert_called_once_with( - message, - ctx.channel, - target or ctx.channel, - alert_target=was_silenced - ) + with self.subTest(was_silenced=was_silenced, target=target, message=message): + with mock.patch.object(self.cog, "send_message") as send_message: + ctx = MockContext() + await self.cog.silence.callback(self.cog, ctx, duration, target) + send_message.assert_called_once_with( + message, + ctx.channel, + target or ctx.channel, + alert_target=was_silenced + ) @voice_sync_helper async def test_sync_called(self, ctx, sync, kick): @@ -603,21 +605,21 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): (False, silence.MSG_UNSILENCE_MANUAL, PermissionOverwrite(add_reactions=False)), ) - for was_unsilenced, message, overwrite in test_cases: - ctx = MockContext() + targets = (None, MockTextChannel()) - for target in [None, MockTextChannel()]: - ctx.channel.overwrites_for.return_value = overwrite - if target: - target.overwrites_for.return_value = overwrite + for (was_unsilenced, message, overwrite), target in itertools.product(test_cases, targets): + ctx = MockContext() + ctx.channel.overwrites_for.return_value = overwrite + if target: + target.overwrites_for.return_value = overwrite - with mock.patch.object(self.cog, "_unsilence", return_value=was_unsilenced): - with mock.patch.object(self.cog, "send_message") as send_message: - with self.subTest(was_unsilenced=was_unsilenced, overwrite=overwrite, target=target): - await self.cog.unsilence.callback(self.cog, ctx, channel=target) + with mock.patch.object(self.cog, "_unsilence", return_value=was_unsilenced): + with mock.patch.object(self.cog, "send_message") as send_message: + with self.subTest(was_unsilenced=was_unsilenced, overwrite=overwrite, target=target): + await self.cog.unsilence.callback(self.cog, ctx, channel=target) - call_args = (message, ctx.channel, target or ctx.channel) - send_message.assert_awaited_once_with(*call_args, alert_target=was_unsilenced) + call_args = (message, ctx.channel, target or ctx.channel) + send_message.assert_awaited_once_with(*call_args, alert_target=was_unsilenced) async def test_skipped_already_unsilenced(self): """Permissions were not set and `False` was returned for an already unsilenced channel.""" -- cgit v1.2.3 From 2f772082f288671354b6bbd71d9a9caad6fe87af Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Wed, 12 May 2021 00:03:26 +0300 Subject: Updates Silence To Use `.format` Uses `.format` to create silence and unsilence messages instead of `.replace`. Signed-off-by: Hassan Abouelela --- bot/exts/moderation/silence.py | 20 +++++++++++--------- tests/bot/exts/moderation/test_silence.py | 19 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 2012d75d9..e5c96e76f 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -19,17 +19,17 @@ log = logging.getLogger(__name__) LOCK_NAMESPACE = "silence" -MSG_SILENCE_FAIL = f"{constants.Emojis.cross_mark} current channel is already silenced." -MSG_SILENCE_PERMANENT = f"{constants.Emojis.check_mark} silenced current channel indefinitely." -MSG_SILENCE_SUCCESS = f"{constants.Emojis.check_mark} silenced current channel for {{duration}} minute(s)." +MSG_SILENCE_FAIL = f"{constants.Emojis.cross_mark} {{channel}} is already silenced." +MSG_SILENCE_PERMANENT = f"{constants.Emojis.check_mark} silenced {{channel}} indefinitely." +MSG_SILENCE_SUCCESS = f"{constants.Emojis.check_mark} silenced {{{{channel}}}} for {{duration}} minute(s)." -MSG_UNSILENCE_FAIL = f"{constants.Emojis.cross_mark} current channel was not silenced." +MSG_UNSILENCE_FAIL = f"{constants.Emojis.cross_mark} {{channel}} was not silenced." MSG_UNSILENCE_MANUAL = ( - f"{constants.Emojis.cross_mark} current channel was not unsilenced because the current overwrites were " + f"{constants.Emojis.cross_mark} {{channel}} was not unsilenced because the current overwrites were " f"set manually or the cache was prematurely cleared. " f"Please edit the overwrites manually to unsilence." ) -MSG_UNSILENCE_SUCCESS = f"{constants.Emojis.check_mark} unsilenced current channel." +MSG_UNSILENCE_SUCCESS = f"{constants.Emojis.check_mark} unsilenced {{channel}}." TextOrVoiceChannel = Union[TextChannel, VoiceChannel] @@ -135,7 +135,9 @@ class Silence(commands.Cog): # Reply to invocation channel source_reply = message if source_channel != target_channel: - source_reply = source_reply.replace("current channel", target_channel.mention) + source_reply = source_reply.format(channel=target_channel.mention) + else: + source_reply = source_reply.format(channel="current channel") await source_channel.send(source_reply) # Reply to target channel @@ -143,10 +145,10 @@ class Silence(commands.Cog): if isinstance(target_channel, VoiceChannel): voice_chat = self.bot.get_channel(VOICE_CHANNELS.get(target_channel.id)) if voice_chat and source_channel != voice_chat: - await voice_chat.send(message.replace("current channel", target_channel.mention)) + await voice_chat.send(message.format(channel=target_channel.mention)) elif source_channel != target_channel: - await target_channel.send(message) + await target_channel.send(message.format(channel="current channel")) @commands.command(aliases=("hush",)) @lock(LOCK_NAMESPACE, _select_lock_channel, raise_error=True) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index de7230ae5..af6dd5a37 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -792,21 +792,20 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): async def test_duration_replacement(self): """Tests that the channel name was set correctly for one target channel.""" - message = "Current. The following should be replaced: current channel." + message = "Current. The following should be replaced: {channel}." await self.cog.send_message(message, *self.text_channels, alert_target=False) - updated_message = message.replace("current channel", self.text_channels[0].mention) + updated_message = message.format(channel=self.text_channels[0].mention) self.text_channels[0].send.assert_awaited_once_with(updated_message) self.text_channels[1].send.assert_not_called() async def test_name_replacement_multiple_channels(self): """Tests that the channel name was set correctly for two channels.""" - message = "Current. The following should be replaced: current channel." + message = "Current. The following should be replaced: {channel}." await self.cog.send_message(message, *self.text_channels, alert_target=True) - updated_message = message.replace("current channel", self.text_channels[0].mention) - self.text_channels[0].send.assert_awaited_once_with(updated_message) - self.text_channels[1].send.assert_awaited_once_with(message) + self.text_channels[0].send.assert_awaited_once_with(message.format(channel=self.text_channels[0].mention)) + self.text_channels[1].send.assert_awaited_once_with(message.format(channel="current channel")) async def test_silence_voice(self): """Tests that the correct message was sent when a voice channel is muted without alerting.""" @@ -820,10 +819,10 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): with unittest.mock.patch.object(silence, "VOICE_CHANNELS") as mock_voice_channels: mock_voice_channels.get.return_value = self.text_channels[1].id - message = "This should show up as current channel." + message = "This should show up as {channel}." await self.cog.send_message(message, self.text_channels[0], self.voice_channel, alert_target=True) - updated_message = message.replace("current channel", self.voice_channel.mention) + updated_message = message.format(channel=self.voice_channel.mention) self.text_channels[0].send.assert_awaited_once_with(updated_message) self.text_channels[1].send.assert_awaited_once_with(updated_message) @@ -834,10 +833,10 @@ class SendMessageTests(unittest.IsolatedAsyncioTestCase): with unittest.mock.patch.object(silence, "VOICE_CHANNELS") as mock_voice_channels: mock_voice_channels.get.return_value = self.text_channels[1].id - message = "This should show up as current channel." + message = "This should show up as {channel}." await self.cog.send_message(message, self.text_channels[1], self.voice_channel, alert_target=True) - updated_message = message.replace("current channel", self.voice_channel.mention) + updated_message = message.format(channel=self.voice_channel.mention) self.text_channels[1].send.assert_awaited_once_with(updated_message) mock_voice_channels.get.assert_called_once_with(self.voice_channel.id) -- cgit v1.2.3 From 0fe7755733bafefe5f4dfba6458f4dbec27ac9f4 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Thu, 13 May 2021 00:32:58 +0300 Subject: Updates Silence To Accept Duration Or Channel Updates the silence command to accept the silence duration or channel as the first argument to the command. Updates tests. Signed-off-by: Hassan Abouelela --- bot/exts/moderation/silence.py | 35 +++++++++--- tests/bot/exts/moderation/test_silence.py | 91 ++++++++++++++++++++++++++++--- 2 files changed, 109 insertions(+), 17 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index e5c96e76f..8e4ce7ae2 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -1,5 +1,6 @@ import json import logging +import typing from contextlib import suppress from datetime import datetime, timedelta, timezone from typing import Optional, OrderedDict, Union @@ -84,12 +85,8 @@ class SilenceNotifier(tasks.Loop): async def _select_lock_channel(args: OrderedDict[str, any]) -> TextOrVoiceChannel: """Passes the channel to be silenced to the resource lock.""" - channel = args["channel"] - if channel is not None: - return channel - - else: - return args["ctx"].channel + channel, _ = Silence.parse_silence_args(args["ctx"], args["duration_or_channel"], args["duration"]) + return channel class Silence(commands.Cog): @@ -155,8 +152,8 @@ class Silence(commands.Cog): async def silence( self, ctx: Context, + duration_or_channel: typing.Union[TextOrVoiceChannel, HushDurationConverter] = None, duration: HushDurationConverter = 10, - channel: TextOrVoiceChannel = None, *, kick: bool = False ) -> None: @@ -170,8 +167,8 @@ class Silence(commands.Cog): If `kick` is True, members will not be added back to the voice channel, and members will be unable to rejoin. """ await self._init_task - if channel is None: - channel = ctx.channel + channel, duration = self.parse_silence_args(ctx, duration_or_channel, duration) + channel_info = f"#{channel} ({channel.id})" log.debug(f"{ctx.author} is silencing channel {channel_info}.") @@ -198,6 +195,26 @@ class Silence(commands.Cog): formatted_message = MSG_SILENCE_SUCCESS.format(duration=duration) await self.send_message(formatted_message, ctx.channel, channel, alert_target=True) + @staticmethod + def parse_silence_args( + ctx: Context, + duration_or_channel: typing.Union[TextOrVoiceChannel, int], + duration: HushDurationConverter + ) -> typing.Tuple[TextOrVoiceChannel, int]: + """Helper method to parse the arguments of the silence command.""" + duration: int + + if duration_or_channel: + if isinstance(duration_or_channel, (TextChannel, VoiceChannel)): + channel = duration_or_channel + else: + channel = ctx.channel + duration = duration_or_channel + else: + channel = ctx.channel + + return channel, duration + async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, *, kick: bool = False) -> bool: """Set silence permission overwrites for `channel` and return True if successful.""" # Get the original channel overwrites diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index af6dd5a37..a7ea733c5 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -260,6 +260,81 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(member.move_to.call_count, 1 if member == failing_member else 2) +class SilenceArgumentParserTests(unittest.IsolatedAsyncioTestCase): + """Tests for the silence argument parser utility function.""" + + def setUp(self): + self.bot = MockBot() + self.cog = silence.Silence(self.bot) + self.cog._init_task = asyncio.Future() + self.cog._init_task.set_result(None) + + @autospec(silence.Silence, "send_message", pass_mocks=False) + @autospec(silence.Silence, "_set_silence_overwrites", return_value=False, pass_mocks=False) + @autospec(silence.Silence, "parse_silence_args") + async def test_command(self, parser_mock): + """Test that the command passes in the correct arguments for different calls.""" + test_cases = ( + (), + (15, ), + (MockTextChannel(),), + (MockTextChannel(), 15), + ) + + ctx = MockContext() + parser_mock.return_value = (ctx.channel, 10) + + for case in test_cases: + with self.subTest("Test command converters", args=case): + await self.cog.silence.callback(self.cog, ctx, *case) + + try: + first_arg = case[0] + except IndexError: + # Default value when the first argument is not passed + first_arg = None + + try: + second_arg = case[1] + except IndexError: + # Default value when the second argument is not passed + second_arg = 10 + + parser_mock.assert_called_with(ctx, first_arg, second_arg) + + async def test_no_arguments(self): + """Test the parser when no arguments are passed to the command.""" + ctx = MockContext() + channel, duration = self.cog.parse_silence_args(ctx, None, 10) + + self.assertEqual(ctx.channel, channel) + self.assertEqual(10, duration) + + async def test_channel_only(self): + """Test the parser when just the channel argument is passed.""" + expected_channel = MockTextChannel() + actual_channel, duration = self.cog.parse_silence_args(MockContext(), expected_channel, 10) + + self.assertEqual(expected_channel, actual_channel) + self.assertEqual(10, duration) + + async def test_duration_only(self): + """Test the parser when just the duration argument is passed.""" + ctx = MockContext() + channel, duration = self.cog.parse_silence_args(ctx, 15, 10) + + self.assertEqual(ctx.channel, channel) + self.assertEqual(15, duration) + + async def test_all_args(self): + """Test the parser when both channel and duration are passed.""" + expected_channel = MockTextChannel() + actual_channel, duration = self.cog.parse_silence_args(MockContext(), expected_channel, 15) + + self.assertEqual(expected_channel, actual_channel) + self.assertEqual(15, duration) + + @autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) class RescheduleTests(unittest.IsolatedAsyncioTestCase): """Tests for the rescheduling of cached unsilences.""" @@ -387,7 +462,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): with self.subTest(was_silenced=was_silenced, target=target, message=message): with mock.patch.object(self.cog, "send_message") as send_message: ctx = MockContext() - await self.cog.silence.callback(self.cog, ctx, duration, target) + await self.cog.silence.callback(self.cog, ctx, target, duration) send_message.assert_called_once_with( message, ctx.channel, @@ -399,7 +474,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_sync_called(self, ctx, sync, kick): """Tests if silence command calls sync on a voice channel.""" channel = MockVoiceChannel() - await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=False) + await self.cog.silence.callback(self.cog, ctx, channel, 10, kick=False) sync.assert_awaited_once_with(self.cog, channel) kick.assert_not_called() @@ -408,7 +483,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_kick_called(self, ctx, sync, kick): """Tests if silence command calls kick on a voice channel.""" channel = MockVoiceChannel() - await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=True) + await self.cog.silence.callback(self.cog, ctx, channel, 10, kick=True) kick.assert_awaited_once_with(channel) sync.assert_not_called() @@ -417,7 +492,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_sync_not_called(self, ctx, sync, kick): """Tests that silence command does not call sync on a text channel.""" channel = MockTextChannel() - await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=False) + await self.cog.silence.callback(self.cog, ctx, channel, 10, kick=False) sync.assert_not_called() kick.assert_not_called() @@ -426,7 +501,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_kick_not_called(self, ctx, sync, kick): """Tests that silence command does not call kick on a text channel.""" channel = MockTextChannel() - await self.cog.silence.callback(self.cog, ctx, 10, channel, kick=True) + await self.cog.silence.callback(self.cog, ctx, channel, 10, kick=True) sync.assert_not_called() kick.assert_not_called() @@ -515,7 +590,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_indefinite_added_to_notifier(self): """Channel was added to notifier if a duration was not set for the silence.""" with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=True): - await self.cog.silence.callback(self.cog, MockContext(), None) + await self.cog.silence.callback(self.cog, MockContext(), None, None) self.cog.notifier.add_channel.assert_called_once() async def test_silenced_not_added_to_notifier(self): @@ -547,7 +622,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_cached_indefinite_time(self): """A value of -1 was cached for a permanent silence.""" ctx = MockContext(channel=self.text_channel) - await self.cog.silence.callback(self.cog, ctx, None) + await self.cog.silence.callback(self.cog, ctx, None, None) self.cog.unsilence_timestamps.set.assert_awaited_once_with(ctx.channel.id, -1) async def test_scheduled_task(self): @@ -563,7 +638,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_permanent_not_scheduled(self): """A task was not scheduled for a permanent silence.""" ctx = MockContext(channel=self.text_channel) - await self.cog.silence.callback(self.cog, ctx, None) + await self.cog.silence.callback(self.cog, ctx, None, None) self.cog.scheduler.schedule_later.assert_not_called() -- cgit v1.2.3 From 50294816787562abd5f3f984f513d039612d2f3b Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Thu, 13 May 2021 06:13:31 +0300 Subject: Updates Shh Command To Mirror Silence Updates the shh and unshh commands from the error handler to accept channel and kick arguments, to give them the same interface as the silence and unsilence command. Signed-off-by: Hassan Abouelela --- bot/exts/backend/error_handler.py | 27 ++++++++- tests/bot/exts/backend/test_error_handler.py | 84 ++++++++++++++++++++++++---- 2 files changed, 98 insertions(+), 13 deletions(-) (limited to 'tests') diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index d8de177f5..a3c04437f 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -3,7 +3,7 @@ import logging import typing as t from discord import Embed -from discord.ext.commands import Cog, Context, errors +from discord.ext.commands import ChannelNotFound, Cog, Context, TextChannelConverter, VoiceChannelConverter, errors from sentry_sdk import push_scope from bot.api import ResponseCodeError @@ -115,8 +115,10 @@ class ErrorHandler(Cog): Return bool depending on success of command. """ command = ctx.invoked_with.lower() + args = ctx.message.content.lower().split(" ") silence_command = self.bot.get_command("silence") ctx.invoked_from_error_handler = True + try: if not await silence_command.can_run(ctx): log.debug("Cancelling attempt to invoke silence/unsilence due to failed checks.") @@ -124,11 +126,30 @@ class ErrorHandler(Cog): except errors.CommandError: log.debug("Cancelling attempt to invoke silence/unsilence due to failed checks.") return False + + # Parse optional args + channel = None + duration = min(command.count("h") * 2, 15) + kick = False + + if len(args) > 1: + # Parse channel + for converter in (TextChannelConverter(), VoiceChannelConverter()): + try: + channel = await converter.convert(ctx, args[1]) + break + except ChannelNotFound: + continue + + if len(args) > 2 and channel is not None: + # Parse kick + kick = args[2].lower() == "true" + if command.startswith("shh"): - await ctx.invoke(silence_command, duration=min(command.count("h")*2, 15)) + await ctx.invoke(silence_command, duration_or_channel=channel, duration=duration, kick=kick) return True elif command.startswith("unshh"): - await ctx.invoke(self.bot.get_command("unsilence")) + await ctx.invoke(self.bot.get_command("unsilence"), channel=channel) return True return False diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index bd4fb5942..37e8108fc 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.error_handler import ErrorHandler, setup 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 +from tests.helpers import MockBot, MockContext, MockGuild, MockRole, MockTextChannel class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): @@ -226,8 +226,8 @@ class TrySilenceTests(unittest.IsolatedAsyncioTestCase): self.bot.get_command.return_value.can_run = AsyncMock(side_effect=errors.CommandError()) self.assertFalse(await self.cog.try_silence(self.ctx)) - async def test_try_silence_silencing(self): - """Should run silence command with correct arguments.""" + async def test_try_silence_silence_duration(self): + """Should run silence command with correct duration argument.""" self.bot.get_command.return_value.can_run = AsyncMock(return_value=True) test_cases = ("shh", "shhh", "shhhhhh", "shhhhhhhhhhhhhhhhhhh") @@ -238,21 +238,85 @@ class TrySilenceTests(unittest.IsolatedAsyncioTestCase): self.assertTrue(await self.cog.try_silence(self.ctx)) self.ctx.invoke.assert_awaited_once_with( self.bot.get_command.return_value, - duration=min(case.count("h")*2, 15) + duration_or_channel=None, + duration=min(case.count("h")*2, 15), + kick=False ) + async def test_try_silence_silence_arguments(self): + """Should run silence with the correct channel, duration, and kick arguments.""" + self.bot.get_command.return_value.can_run = AsyncMock(return_value=True) + + test_cases = ( + (MockTextChannel(), None), # None represents the case when no argument is passed + (MockTextChannel(), False), + (MockTextChannel(), True) + ) + + for channel, kick in test_cases: + with self.subTest(kick=kick, channel=channel): + self.ctx.reset_mock() + self.ctx.invoked_with = "shh" + + self.ctx.message.content = f"!shh {channel.name} {kick if kick is not None else ''}" + self.ctx.guild.text_channels = [channel] + + self.assertTrue(await self.cog.try_silence(self.ctx)) + self.ctx.invoke.assert_awaited_once_with( + self.bot.get_command.return_value, + duration_or_channel=channel, + duration=4, + kick=(kick if kick is not None else False) + ) + + async def test_try_silence_silence_message(self): + """If the words after the command could not be converted to a channel, None should be passed as channel.""" + self.bot.get_command.return_value.can_run = AsyncMock(return_value=True) + self.ctx.invoked_with = "shh" + self.ctx.message.content = "!shh not_a_channel true" + + self.assertTrue(await self.cog.try_silence(self.ctx)) + self.ctx.invoke.assert_awaited_once_with( + self.bot.get_command.return_value, + duration_or_channel=None, + duration=4, + kick=False + ) + async def test_try_silence_unsilence(self): - """Should call unsilence command.""" + """Should call unsilence command with correct duration and channel arguments.""" self.silence.silence.can_run = AsyncMock(return_value=True) - test_cases = ("unshh", "unshhhhh", "unshhhhhhhhh") + test_cases = ( + ("unshh", None), + ("unshhhhh", None), + ("unshhhhhhhhh", None), + ("unshh", MockTextChannel()) + ) - for case in test_cases: - with self.subTest(message=case): + for invoke, channel in test_cases: + with self.subTest(message=invoke, channel=channel): self.bot.get_command.side_effect = (self.silence.silence, self.silence.unsilence) self.ctx.reset_mock() - self.ctx.invoked_with = case + + self.ctx.invoked_with = invoke + self.ctx.message.content = f"!{invoke}" + if channel is not None: + self.ctx.message.content += f" {channel.name}" + self.ctx.guild.text_channels = [channel] + self.assertTrue(await self.cog.try_silence(self.ctx)) - self.ctx.invoke.assert_awaited_once_with(self.silence.unsilence) + self.ctx.invoke.assert_awaited_once_with(self.silence.unsilence, channel=channel) + + async def test_try_silence_unsilence_message(self): + """If the words after the command could not be converted to a channel, None should be passed as channel.""" + self.silence.silence.can_run = AsyncMock(return_value=True) + self.bot.get_command.side_effect = (self.silence.silence, self.silence.unsilence) + + self.ctx.invoked_with = "unshh" + self.ctx.message.content = "!unshh not_a_channel" + + self.assertTrue(await self.cog.try_silence(self.ctx)) + self.ctx.invoke.assert_awaited_once_with(self.silence.unsilence, channel=None) async def test_try_silence_no_match(self): """Should return `False` when message don't match.""" -- cgit v1.2.3 From f92338ef18d1bc5d11405a5d9e6ede4f9e080110 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 5 Jun 2021 14:04:06 +0300 Subject: Properly Handles Indefinite Silences Fixes a bug that stopped the duration `forever` from getting used as a valid duration for silence. Signed-off-by: Hassan Abouelela --- bot/converters.py | 6 +++--- bot/exts/moderation/silence.py | 7 +++++-- tests/bot/exts/moderation/test_silence.py | 7 +++++++ tests/bot/test_converters.py | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) (limited to 'tests') diff --git a/bot/converters.py b/bot/converters.py index 2a3943831..595809517 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -416,11 +416,11 @@ class HushDurationConverter(Converter): MINUTES_RE = re.compile(r"(\d+)(?:M|m|$)") - async def convert(self, ctx: Context, argument: str) -> t.Optional[int]: + async def convert(self, ctx: Context, argument: str) -> int: """ Convert `argument` to a duration that's max 15 minutes or None. - If `"forever"` is passed, None is returned; otherwise an int of the extracted time. + If `"forever"` is passed, -1 is returned; otherwise an int of the extracted time. Accepted formats are: * , * m, @@ -428,7 +428,7 @@ class HushDurationConverter(Converter): * forever. """ if argument == "forever": - return None + return -1 match = self.MINUTES_RE.match(argument) if not match: raise BadArgument(f"{argument} is not a valid minutes duration.") diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 8e4ce7ae2..8025f3df6 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -200,9 +200,9 @@ class Silence(commands.Cog): ctx: Context, duration_or_channel: typing.Union[TextOrVoiceChannel, int], duration: HushDurationConverter - ) -> typing.Tuple[TextOrVoiceChannel, int]: + ) -> typing.Tuple[TextOrVoiceChannel, Optional[int]]: """Helper method to parse the arguments of the silence command.""" - duration: int + duration: Optional[int] if duration_or_channel: if isinstance(duration_or_channel, (TextChannel, VoiceChannel)): @@ -213,6 +213,9 @@ class Silence(commands.Cog): else: channel = ctx.channel + if duration == -1: + duration = None + return channel, duration async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, *, kick: bool = False) -> bool: diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index a7ea733c5..59a5893ef 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -641,6 +641,13 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog.silence.callback(self.cog, ctx, None, None) self.cog.scheduler.schedule_later.assert_not_called() + async def test_indefinite_silence(self): + """Test silencing a channel forever.""" + with mock.patch.object(self.cog, "_schedule_unsilence") as unsilence: + ctx = MockContext(channel=self.text_channel) + await self.cog.silence.callback(self.cog, ctx, -1) + unsilence.assert_awaited_once_with(ctx, ctx.channel, None) + @autospec(silence.Silence, "unsilence_timestamps", pass_mocks=False) class UnsilenceTests(unittest.IsolatedAsyncioTestCase): diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py index 4af84dde5..2a1c4e543 100644 --- a/tests/bot/test_converters.py +++ b/tests/bot/test_converters.py @@ -291,7 +291,7 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase): ("10", 10), ("5m", 5), ("5M", 5), - ("forever", None), + ("forever", -1), ) converter = HushDurationConverter() for minutes_string, expected_minutes in test_values: -- cgit v1.2.3 From 4edecf659c3148c8e4427054b7d841c65d0f67be Mon Sep 17 00:00:00 2001 From: Xithrius Date: Sun, 6 Jun 2021 00:11:41 -0700 Subject: Added .txt file extension to antimalware test. --- tests/bot/exts/filters/test_antimalware.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/exts/filters/test_antimalware.py b/tests/bot/exts/filters/test_antimalware.py index 3393c6cdc..9f020c964 100644 --- a/tests/bot/exts/filters/test_antimalware.py +++ b/tests/bot/exts/filters/test_antimalware.py @@ -118,7 +118,10 @@ class AntiMalwareCogTests(unittest.IsolatedAsyncioTestCase): cmd_channel = self.bot.get_channel(Channels.bot_commands) self.assertEqual(embed.description, antimalware.TXT_EMBED_DESCRIPTION.format.return_value) - antimalware.TXT_EMBED_DESCRIPTION.format.assert_called_with(cmd_channel_mention=cmd_channel.mention) + antimalware.TXT_EMBED_DESCRIPTION.format.assert_called_with( + blocked_extension_str=".txt", + cmd_channel_mention=cmd_channel.mention + ) async def test_other_disallowed_extension_embed_description(self): """Test the description for a non .py/.txt disallowed extension.""" -- cgit v1.2.3 From d510a6af7d6158009ef23fefd44f1e06bdb33876 Mon Sep 17 00:00:00 2001 From: Xithrius Date: Sun, 6 Jun 2021 00:34:20 -0700 Subject: Added subtests for `.txt`, `.json`, and `.csv` files. --- tests/bot/exts/filters/test_antimalware.py | 44 +++++++++++++++++++----------- 1 file changed, 28 insertions(+), 16 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/filters/test_antimalware.py b/tests/bot/exts/filters/test_antimalware.py index 9f020c964..359401814 100644 --- a/tests/bot/exts/filters/test_antimalware.py +++ b/tests/bot/exts/filters/test_antimalware.py @@ -105,24 +105,36 @@ class AntiMalwareCogTests(unittest.IsolatedAsyncioTestCase): async def test_txt_file_redirect_embed_description(self): """A message containing a .txt file should result in the correct embed.""" - attachment = MockAttachment(filename="python.txt") - self.message.attachments = [attachment] - self.message.channel.send = AsyncMock() - antimalware.TXT_EMBED_DESCRIPTION = Mock() - antimalware.TXT_EMBED_DESCRIPTION.format.return_value = "test" - - await self.cog.on_message(self.message) - self.message.channel.send.assert_called_once() - args, kwargs = self.message.channel.send.call_args - embed = kwargs.pop("embed") - cmd_channel = self.bot.get_channel(Channels.bot_commands) - - self.assertEqual(embed.description, antimalware.TXT_EMBED_DESCRIPTION.format.return_value) - antimalware.TXT_EMBED_DESCRIPTION.format.assert_called_with( - blocked_extension_str=".txt", - cmd_channel_mention=cmd_channel.mention + test_values = ( + ("text", ".txt"), + ("json", ".json"), + ("csv", ".csv"), ) + for file_name, disallowed_extension in test_values: + with self.subTest(file_name=file_name, disallowed_extension=disallowed_extension): + + attachment = MockAttachment(filename=f"{file_name}{disallowed_extension}") + self.message.attachments = [attachment] + self.message.channel.send = AsyncMock() + antimalware.TXT_EMBED_DESCRIPTION = Mock() + antimalware.TXT_EMBED_DESCRIPTION.format.return_value = "test" + + await self.cog.on_message(self.message) + self.message.channel.send.assert_called_once() + args, kwargs = self.message.channel.send.call_args + embed = kwargs.pop("embed") + cmd_channel = self.bot.get_channel(Channels.bot_commands) + + self.assertEqual( + embed.description, + antimalware.TXT_EMBED_DESCRIPTION.format.return_value + ) + antimalware.TXT_EMBED_DESCRIPTION.format.assert_called_with( + blocked_extension_str=disallowed_extension, + cmd_channel_mention=cmd_channel.mention + ) + async def test_other_disallowed_extension_embed_description(self): """Test the description for a non .py/.txt disallowed extension.""" attachment = MockAttachment(filename="python.disallowed") -- cgit v1.2.3 From aeaef8ff604c9ea62fdf1602200ee87f2adf7f6a Mon Sep 17 00:00:00 2001 From: Xithrius Date: Sun, 6 Jun 2021 15:55:19 -0700 Subject: Added new formats to unittest docstrings. --- tests/bot/exts/filters/test_antimalware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/filters/test_antimalware.py b/tests/bot/exts/filters/test_antimalware.py index 359401814..c07bde8d7 100644 --- a/tests/bot/exts/filters/test_antimalware.py +++ b/tests/bot/exts/filters/test_antimalware.py @@ -104,7 +104,7 @@ class AntiMalwareCogTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(embed.description, antimalware.PY_EMBED_DESCRIPTION) async def test_txt_file_redirect_embed_description(self): - """A message containing a .txt file should result in the correct embed.""" + """A message containing a .txt/.json/.csv file should result in the correct embed.""" test_values = ( ("text", ".txt"), ("json", ".json"), @@ -136,7 +136,7 @@ class AntiMalwareCogTests(unittest.IsolatedAsyncioTestCase): ) async def test_other_disallowed_extension_embed_description(self): - """Test the description for a non .py/.txt disallowed extension.""" + """Test the description for a non .py/.txt/.json/.csv disallowed extension.""" attachment = MockAttachment(filename="python.disallowed") self.message.attachments = [attachment] self.message.channel.send = AsyncMock() -- cgit v1.2.3 From a305d3983350fbf30b873fce76a44707b549fd55 Mon Sep 17 00:00:00 2001 From: Xithrius Date: Sun, 6 Jun 2021 15:57:27 -0700 Subject: Renamed blocked_extension_str to blocked_extension. --- bot/exts/filters/antimalware.py | 4 ++-- tests/bot/exts/filters/test_antimalware.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/bot/exts/filters/antimalware.py b/bot/exts/filters/antimalware.py index f8d303389..89e539e7b 100644 --- a/bot/exts/filters/antimalware.py +++ b/bot/exts/filters/antimalware.py @@ -18,7 +18,7 @@ PY_EMBED_DESCRIPTION = ( TXT_LIKE_FILES = {".txt", ".csv", ".json"} TXT_EMBED_DESCRIPTION = ( "**Uh-oh!** It looks like your message got zapped by our spam filter. " - "We currently don't allow `{blocked_extension_str}` attachments, " + "We currently don't allow `{blocked_extension}` attachments, " "so here are some tips to help you travel safely: \n\n" "• If you attempted to send a message longer than 2000 characters, try shortening your message " "to fit within the character limit or use a pasting service (see below) \n\n" @@ -76,7 +76,7 @@ class AntiMalware(Cog): # Work around Discord AutoConversion of messages longer than 2000 chars to .txt cmd_channel = self.bot.get_channel(Channels.bot_commands) embed.description = TXT_EMBED_DESCRIPTION.format( - blocked_extension_str=extensions.pop(), + blocked_extension=extensions.pop(), cmd_channel_mention=cmd_channel.mention ) elif extensions_blocked: diff --git a/tests/bot/exts/filters/test_antimalware.py b/tests/bot/exts/filters/test_antimalware.py index c07bde8d7..06d78de9d 100644 --- a/tests/bot/exts/filters/test_antimalware.py +++ b/tests/bot/exts/filters/test_antimalware.py @@ -131,7 +131,7 @@ class AntiMalwareCogTests(unittest.IsolatedAsyncioTestCase): antimalware.TXT_EMBED_DESCRIPTION.format.return_value ) antimalware.TXT_EMBED_DESCRIPTION.format.assert_called_with( - blocked_extension_str=disallowed_extension, + blocked_extension=disallowed_extension, cmd_channel_mention=cmd_channel.mention ) -- cgit v1.2.3 From 13442f859f452578397766dedc7904928794610a Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Mon, 7 Jun 2021 05:28:07 +0300 Subject: Switches To Pytest As Test Runner Switches the test runner from unittest to pytest, to allow the usage of plugins such as xdist. This commit also adds pytest-cov purely as a generator for .coverage files. Signed-off-by: Hassan Abouelela --- .coveragerc | 5 - .github/workflows/lint-test.yml | 5 +- poetry.lock | 340 +++++++++++++++++++++++++++++++--------- pyproject.toml | 5 +- tests/README.md | 6 +- 5 files changed, 277 insertions(+), 84 deletions(-) delete mode 100644 .coveragerc (limited to 'tests') diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index d572bd705..000000000 --- a/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[run] -branch = true -source = - bot - tests diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index d96f324ec..370b0b38b 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -97,12 +97,9 @@ jobs: --format='::error file=%(path)s,line=%(row)d,col=%(col)d::\ [flake8] %(code)s: %(text)s'" - # We run `coverage` using the `python` command so we can suppress - # irrelevant warnings in our CI output. - name: Run tests and generate coverage report run: | - python -Wignore -m coverage run -m unittest - coverage report -m + pytest -n auto --cov bot --disable-warnings -q # This step will publish the coverage reports coveralls.io and # print a "job" link in the output of the GitHub Action diff --git a/poetry.lock b/poetry.lock index ba8b7af4b..a671d8a35 100644 --- a/poetry.lock +++ b/poetry.lock @@ -82,6 +82,14 @@ yarl = "*" [package.extras] develop = ["aiomisc (>=11.0,<12.0)", "async-generator", "coverage (!=4.3)", "coveralls", "pylava", "pytest", "pytest-cov", "tox (>=2.4)"] +[[package]] +name = "apipkg" +version = "1.5" +description = "apipkg: namespace control and lazy-import mechanism" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "appdirs" version = "1.4.4" @@ -124,6 +132,14 @@ category = "main" optional = false python-versions = ">=3.5.3" +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "attrs" version = "21.2.0" @@ -155,7 +171,7 @@ lxml = ["lxml"] [[package]] name = "certifi" -version = "2020.12.5" +version = "2021.5.30" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -174,7 +190,7 @@ pycparser = "*" [[package]] name = "cfgv" -version = "3.2.0" +version = "3.3.0" description = "Validate configuration and produce human readable error messages." category = "dev" optional = false @@ -268,7 +284,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [[package]] name = "distlib" -version = "0.3.1" +version = "0.3.2" description = "Distribution utilities" category = "dev" optional = false @@ -293,9 +309,23 @@ python-versions = "*" [package.extras] dev = ["pytest", "coverage", "coveralls"] +[[package]] +name = "execnet" +version = "1.8.1" +description = "execnet: rapid multi-Python deployment" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +apipkg = ">=1.4" + +[package.extras] +testing = ["pre-commit"] + [[package]] name = "fakeredis" -version = "1.5.0" +version = "1.5.1" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false @@ -467,7 +497,7 @@ pyreadline = {version = "*", markers = "sys_platform == \"win32\""} [[package]] name = "identify" -version = "2.2.4" +version = "2.2.9" description = "File identification library for Python" category = "dev" optional = false @@ -478,11 +508,19 @@ license = ["editdistance-s"] [[package]] name = "idna" -version = "3.1" +version = "3.2" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=3.4" +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" [[package]] name = "lxml" @@ -520,7 +558,7 @@ python-versions = "*" [[package]] name = "more-itertools" -version = "8.7.0" +version = "8.8.0" description = "More routines for operating on iterables, beyond itertools" category = "main" optional = false @@ -558,6 +596,17 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "packaging" +version = "20.9" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" + [[package]] name = "pamqp" version = "2.3.0" @@ -580,9 +629,20 @@ python-versions = "*" [package.dependencies] flake8-polyfill = ">=1.0.2,<2" +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + [[package]] name = "pre-commit" -version = "2.12.1" +version = "2.13.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -607,9 +667,17 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pycares" -version = "3.2.3" +version = "4.0.0" description = "Python interface for c-ares" category = "main" optional = false @@ -639,7 +707,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pydocstyle" -version = "6.0.0" +version = "6.1.1" description = "Python docstring style checker" category = "dev" optional = false @@ -648,6 +716,9 @@ python-versions = ">=3.6" [package.dependencies] snowballstemmer = "*" +[package.extras] +toml = ["toml"] + [[package]] name = "pyflakes" version = "2.3.1" @@ -656,6 +727,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "pyreadline" version = "2.1" @@ -664,6 +743,73 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pytest" +version = "6.2.4" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-forked" +version = "1.3.0" +description = "run tests in isolated forked subprocesses" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +py = "*" +pytest = ">=3.10" + +[[package]] +name = "pytest-xdist" +version = "2.2.1" +description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +execnet = ">=1.1" +psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} +pytest = ">=6.0.0" +pytest-forked = "*" + +[package.extras] +psutil = ["psutil (>=3.0)"] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.8.1" @@ -794,7 +940,7 @@ python-versions = "*" [[package]] name = "sortedcontainers" -version = "2.3.0" +version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" category = "main" optional = false @@ -847,20 +993,20 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.4" +version = "1.26.5" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] +brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -brotli = ["brotlipy (>=0.6.0)"] [[package]] name = "virtualenv" -version = "20.4.6" +version = "20.4.7" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -891,7 +1037,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "ece3b915901a62911ff7ff4a616b3972e815c0e1c7097c8994163af13cadde0e" +content-hash = "c1163e748d2fabcbcc267ea0eeccf4be6dfe5a468d769b6e5bc9023e8ab0a2bf" [metadata.files] aio-pika = [ @@ -953,6 +1099,10 @@ aiormq = [ {file = "aiormq-3.3.1-py3-none-any.whl", hash = "sha256:e584dac13a242589aaf42470fd3006cb0dc5aed6506cbd20357c7ec8bbe4a89e"}, {file = "aiormq-3.3.1.tar.gz", hash = "sha256:8218dd9f7198d6e7935855468326bbacf0089f926c70baa8dd92944cb2496573"}, ] +apipkg = [ + {file = "apipkg-1.5-py2.py3-none-any.whl", hash = "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"}, + {file = "apipkg-1.5.tar.gz", hash = "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6"}, +] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -969,6 +1119,10 @@ async-timeout = [ {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, ] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, @@ -979,8 +1133,8 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"}, ] certifi = [ - {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, - {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] cffi = [ {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, @@ -1022,8 +1176,8 @@ cffi = [ {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, ] cfgv = [ - {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, - {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"}, + {file = "cfgv-3.3.0-py2.py3-none-any.whl", hash = "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"}, + {file = "cfgv-3.3.0.tar.gz", hash = "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1"}, ] chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, @@ -1104,8 +1258,8 @@ deepdiff = [ {file = "discord.py-1.6.0.tar.gz", hash = "sha256:ba8be99ff1b8c616f7b6dcb700460d0222b29d4c11048e74366954c465fdd05f"}, ] distlib = [ - {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, - {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, + {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, + {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, ] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, @@ -1113,9 +1267,13 @@ docopt = [ emoji = [ {file = "emoji-0.6.0.tar.gz", hash = "sha256:e42da4f8d648f8ef10691bc246f682a1ec6b18373abfd9be10ec0b398823bd11"}, ] +execnet = [ + {file = "execnet-1.8.1-py2.py3-none-any.whl", hash = "sha256:e840ce25562e414ee5684864d510dbeeb0bce016bc89b22a6e5ce323b5e6552f"}, + {file = "execnet-1.8.1.tar.gz", hash = "sha256:7e3c2cdb6389542a91e9855a9cc7545fbed679e96f8808bcbb1beb325345b189"}, +] fakeredis = [ - {file = "fakeredis-1.5.0-py3-none-any.whl", hash = "sha256:e0416e4941cecd3089b0d901e60c8dc3c944f6384f5e29e2261c0d3c5fa99669"}, - {file = "fakeredis-1.5.0.tar.gz", hash = "sha256:1ac0cef767c37f51718874a33afb5413e69d132988cb6a80c6e6dbeddf8c7623"}, + {file = "fakeredis-1.5.1-py3-none-any.whl", hash = "sha256:afeb843b031697b3faff0eef8eedadef110741486b37e2bfb95167617785040f"}, + {file = "fakeredis-1.5.1.tar.gz", hash = "sha256:7f85faf640a0da564d8342a7d62936b07f23f4a85f756118fbd35b55f64f281c"}, ] feedparser = [ {file = "feedparser-6.0.2-py3-none-any.whl", hash = "sha256:f596c4b34fb3e2dc7e6ac3a8191603841e8d5d267210064e94d4238737452ddd"}, @@ -1212,12 +1370,16 @@ humanfriendly = [ {file = "humanfriendly-9.1.tar.gz", hash = "sha256:066562956639ab21ff2676d1fda0b5987e985c534fc76700a19bd54bcb81121d"}, ] identify = [ - {file = "identify-2.2.4-py2.py3-none-any.whl", hash = "sha256:ad9f3fa0c2316618dc4d840f627d474ab6de106392a4f00221820200f490f5a8"}, - {file = "identify-2.2.4.tar.gz", hash = "sha256:9bcc312d4e2fa96c7abebcdfb1119563b511b5e3985ac52f60d9116277865b2e"}, + {file = "identify-2.2.9-py2.py3-none-any.whl", hash = "sha256:96c57d493184daecc7299acdeef0ad7771c18a59931ea927942df393688fe849"}, + {file = "identify-2.2.9.tar.gz", hash = "sha256:3a8493cf49cfe4b28d50865e38f942c11be07a7b0aab8e715073e17f145caacc"}, ] idna = [ - {file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"}, - {file = "idna-3.1.tar.gz", hash = "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"}, + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] lxml = [ {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"}, @@ -1276,8 +1438,8 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] more-itertools = [ - {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"}, - {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"}, + {file = "more-itertools-8.8.0.tar.gz", hash = "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a"}, + {file = "more_itertools-8.8.0-py3-none-any.whl", hash = "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d"}, ] mslex = [ {file = "mslex-0.3.0-py2.py3-none-any.whl", hash = "sha256:380cb14abf8fabf40e56df5c8b21a6d533dc5cbdcfe42406bbf08dda8f42e42a"}, @@ -1329,6 +1491,10 @@ nodeenv = [ ordered-set = [ {file = "ordered-set-4.0.2.tar.gz", hash = "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95"}, ] +packaging = [ + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, +] pamqp = [ {file = "pamqp-2.3.0-py2.py3-none-any.whl", hash = "sha256:2f81b5c186f668a67f165193925b6bfd83db4363a6222f599517f29ecee60b02"}, {file = "pamqp-2.3.0.tar.gz", hash = "sha256:5cd0f5a85e89f20d5f8e19285a1507788031cfca4a9ea6f067e3cf18f5e294e8"}, @@ -1337,9 +1503,13 @@ pep8-naming = [ {file = "pep8-naming-0.11.1.tar.gz", hash = "sha256:a1dd47dd243adfe8a83616e27cf03164960b507530f155db94e10b36a6cd6724"}, {file = "pep8_naming-0.11.1-py2.py3-none-any.whl", hash = "sha256:f43bfe3eea7e0d73e8b5d07d6407ab47f2476ccaeff6937c84275cd30b016738"}, ] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] pre-commit = [ - {file = "pre_commit-2.12.1-py2.py3-none-any.whl", hash = "sha256:70c5ec1f30406250b706eda35e868b87e3e4ba099af8787e3e8b4b01e84f4712"}, - {file = "pre_commit-2.12.1.tar.gz", hash = "sha256:900d3c7e1bf4cf0374bb2893c24c23304952181405b4d88c9c40b72bda1bb8a9"}, + {file = "pre_commit-2.13.0-py2.py3-none-any.whl", hash = "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"}, + {file = "pre_commit-2.13.0.tar.gz", hash = "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378"}, ] psutil = [ {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, @@ -1371,40 +1541,44 @@ psutil = [ {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"}, {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"}, ] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] pycares = [ - {file = "pycares-3.2.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ebff743643e54aa70dce0b7098094edefd371641cf79d9c944e9f4a25e9242b0"}, - {file = "pycares-3.2.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:55272411b46787936e8db475b9b6e9b81a8d8cdc253fa8779a45ef979f554fab"}, - {file = "pycares-3.2.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f33ed0e403f98e746f721aeacde917f1bdc7558cb714d713c264848bddff660f"}, - {file = "pycares-3.2.3-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:72807e0c80b705e21c3a39347c12edf43aa4f80373bb37777facf810169372ed"}, - {file = "pycares-3.2.3-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a51df0a8b3eaf225e0dae3a737fd6ce6f3cb2a3bc947e884582fdda9a159d55f"}, - {file = "pycares-3.2.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:663b5c7bd0f66436adac7257ee22ccfe185c3e7830b9bada3d19b79870e1d134"}, - {file = "pycares-3.2.3-cp36-cp36m-win32.whl", hash = "sha256:c2b1e19262ce91c3288b1905b0d41f7ad0fff4b258ce37b517aa2c8d22eb82f1"}, - {file = "pycares-3.2.3-cp36-cp36m-win_amd64.whl", hash = "sha256:e16399654a6c81cfaee2745857c119c20357b5d93de2f169f506b048b5e75d1d"}, - {file = "pycares-3.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88e5131570d7323b29866aa5ac245a9a5788d64677111daa1bde5817acdf012f"}, - {file = "pycares-3.2.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1552ffd823dc595fa8744c996926097a594f4f518d7c147657234b22cf17649d"}, - {file = "pycares-3.2.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f9e28b917373818817aca746238fcd621ec7e4ae9cbc8615f1a045e234eec298"}, - {file = "pycares-3.2.3-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:206d5a652990f10a1f1f3f62bc23d7fe46d99c2dc4b8b8a5101e5a472986cd02"}, - {file = "pycares-3.2.3-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:b8c9670225cdeeeb2b85ea92a807484622ca59f8f578ec73e8ec292515f35a91"}, - {file = "pycares-3.2.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6329160885fc318f80692d4d0a83a8854f9144e7a80c4f25245d0c26f11a4b84"}, - {file = "pycares-3.2.3-cp37-cp37m-win32.whl", hash = "sha256:cd0f7fb40e1169f00b26a12793136bf5c711f155e647cd045a0ce6c98a527b57"}, - {file = "pycares-3.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:a5d419215543d154587590d9d4485e985387ca10c7d3e1a2e5689dd6c0f20e5f"}, - {file = "pycares-3.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54f1c0642935515f27549f09486e72b6b2b1d51ad27a90ce17b760e9ce5e86d"}, - {file = "pycares-3.2.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6ce80eed538dd6106cd7e6136ceb3af10178d1254f07096a827c12e82e5e45c8"}, - {file = "pycares-3.2.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ed972a04067e91f552da84945d38b94c3984c898f699faa8bb066e9f3a114c32"}, - {file = "pycares-3.2.3-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:99a62b101cfb36ab6ebf19cb1ad60db2f9b080dc52db4ca985fe90924f60c758"}, - {file = "pycares-3.2.3-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:2246adcbc948dd31925c9bff5cc41c06fc640f7d982e6b41b6d09e4f201e5c11"}, - {file = "pycares-3.2.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7fd15d3f32be5548f38f95f4762ca73eef9fd623b101218a35d433ee0d4e3b58"}, - {file = "pycares-3.2.3-cp38-cp38-win32.whl", hash = "sha256:4bb0c708d8713741af7c4649d2f11e47c5f4e43131831243aeb18cff512c5469"}, - {file = "pycares-3.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:a53d921956d1e985e510ca0ffa84fbd7ecc6ac7d735d8355cba4395765efcd31"}, - {file = "pycares-3.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0312d25fa9d7c242f66115c4b3ae6ed8aedb457513ba33acef31fa265fc602b4"}, - {file = "pycares-3.2.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9960de8254525d9c3b485141809910c39d5eb1bb8119b1453702aacf72234934"}, - {file = "pycares-3.2.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:929f708a7bb4b2548cbbfc2094b2f90c4d8712056cdc0204788b570ab69c8838"}, - {file = "pycares-3.2.3-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4dd1237f01037cf5b90dd599c7fa79d9d8fb2ab2f401e19213d24228b2d17838"}, - {file = "pycares-3.2.3-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5eea61a74097976502ce377bb75c4fed381d4986bc7fb85e70b691165133d3da"}, - {file = "pycares-3.2.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1c72c0fda4b08924fe04680475350e09b8d210365d950a6dcdde8c449b8d5b98"}, - {file = "pycares-3.2.3-cp39-cp39-win32.whl", hash = "sha256:b1555d51ce29510ffd20f9e0339994dff8c5d1cb093c8e81d5d98f474e345aa7"}, - {file = "pycares-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:43c15138f620ed28e61e51b884490eb8387e5954668f919313753f88dd8134fd"}, - {file = "pycares-3.2.3.tar.gz", hash = "sha256:da1899fde778f9b8736712283eccbf7b654248779b349d139cd28eb30b0fa8cd"}, + {file = "pycares-4.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:db5a533111a3cfd481e7e4fb2bf8bef69f4fa100339803e0504dd5aecafb96a5"}, + {file = "pycares-4.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fdff88393c25016f417770d82678423fc7a56995abb2df3d2a1e55725db6977d"}, + {file = "pycares-4.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0aa97f900a7ffb259be77d640006585e2a907b0cd4edeee0e85cf16605995d5a"}, + {file = "pycares-4.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:a34b0e3e693dceb60b8a1169668d606c75cb100ceba0a2df53c234a0eb067fbc"}, + {file = "pycares-4.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7661d6bbd51a337e7373cb356efa8be9b4655fda484e068f9455e939aec8d54e"}, + {file = "pycares-4.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:57315b8eb8fdbc56b3ad4932bc4b17132bb7c7fd2bd590f7fb84b6b522098aa9"}, + {file = "pycares-4.0.0-cp36-cp36m-win32.whl", hash = "sha256:dca9dc58845a9d083f302732a3130c68ded845ad5d463865d464e53c75a3dd45"}, + {file = "pycares-4.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c95c964d5dd307e104b44b193095c67bb6b10c9eda1ffe7d44ab7a9e84c476d9"}, + {file = "pycares-4.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26e67e4f81c80a5955dcf6193f3d9bee3c491fc0056299b383b84d792252fba4"}, + {file = "pycares-4.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd3011ffd5e1ad55880f7256791dbab9c43ebeda260474a968f19cd0319e1aef"}, + {file = "pycares-4.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1b959dd5921d207d759d421eece1b60416df33a7f862465739d5f2c363c2f523"}, + {file = "pycares-4.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6f258c1b74c048a9501a25f732f11b401564005e5e3c18f1ca6cad0c3dc0fb19"}, + {file = "pycares-4.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:b17ef48729786e62b574c6431f675f4cb02b27691b49e7428a605a50cd59c072"}, + {file = "pycares-4.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:82b3259cb590ddd107a6d2dc52da2a2e9a986bf242e893d58c786af2f8191047"}, + {file = "pycares-4.0.0-cp37-cp37m-win32.whl", hash = "sha256:4876fc790ae32832ae270c4a010a1a77e12ddf8d8e6ad70ad0b0a9d506c985f7"}, + {file = "pycares-4.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f60c04c5561b1ddf85ca4e626943cc09d7fb684e1adb22abb632095415a40fd7"}, + {file = "pycares-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:615406013cdcd1b445e5d1a551d276c6200b3abe77e534f8a7f7e1551208d14f"}, + {file = "pycares-4.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6580aef5d1b29a88c3d72fe73c691eacfd454f86e74d3fdd18f4bad8e8def98b"}, + {file = "pycares-4.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8ebb3ba0485f66cae8eed7ce3e9ed6f2c0bfd5e7319d5d0fbbb511064f17e1d4"}, + {file = "pycares-4.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c5362b7690ca481440f6b98395ac6df06aa50518ccb183c560464d1e5e2ab5d4"}, + {file = "pycares-4.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:eb60be66accc9a9ea1018b591a1f5800cba83491d07e9acc8c56bc6e6607ab54"}, + {file = "pycares-4.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:44896d6e191a6b5a914dbe3aa7c748481bf6ad19a9df33c1e76f8f2dc33fc8f0"}, + {file = "pycares-4.0.0-cp38-cp38-win32.whl", hash = "sha256:09b28fc7bc2cc05f7f69bf1636ddf46086e0a1837b62961e2092fcb40477320d"}, + {file = "pycares-4.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4a5081e232c1d181883dcac4675807f3a6cf33911c4173fbea00c0523687ed4"}, + {file = "pycares-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:103353577a6266a53e71bfee4cf83825f1401fefa60f0fb8bdec35f13be6a5f2"}, + {file = "pycares-4.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ad6caf580ee69806fc6534be93ddbb6e99bf94296d79ab351c37b2992b17abfd"}, + {file = "pycares-4.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3d5e50c95849f6905d2a9dbf02ed03f82580173e3c5604a39e2ad054185631f1"}, + {file = "pycares-4.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:53bc4f181b19576499b02cea4b45391e8dcbe30abd4cd01492f66bfc15615a13"}, + {file = "pycares-4.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:d52f9c725d2a826d5ffa37681eb07ffb996bfe21788590ef257664a3898fc0b5"}, + {file = "pycares-4.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3c7fb8d34ee11971c39acfaf98d0fac66725385ccef3bfe1b174c92b210e1aa4"}, + {file = "pycares-4.0.0-cp39-cp39-win32.whl", hash = "sha256:e9773e07684a55f54657df05237267611a77b294ec3bacb5f851c4ffca38a465"}, + {file = "pycares-4.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:38e54037f36c149146ff15f17a4a963fbdd0f9871d4a21cd94ff9f368140f57e"}, + {file = "pycares-4.0.0.tar.gz", hash = "sha256:d0154fc5753b088758fbec9bc137e1b24bb84fc0c6a09725c8bac25a342311cd"}, ] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, @@ -1415,18 +1589,38 @@ pycparser = [ {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, ] pydocstyle = [ - {file = "pydocstyle-6.0.0-py3-none-any.whl", hash = "sha256:d4449cf16d7e6709f63192146706933c7a334af7c0f083904799ccb851c50f6d"}, - {file = "pydocstyle-6.0.0.tar.gz", hash = "sha256:164befb520d851dbcf0e029681b91f4f599c62c5cd8933fd54b1bfbd50e89e1f"}, + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, ] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] pyreadline = [ {file = "pyreadline-2.1.win-amd64.exe", hash = "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b"}, {file = "pyreadline-2.1.win32.exe", hash = "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e"}, {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, ] +pytest = [ + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +pytest-cov = [ + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, +] +pytest-forked = [ + {file = "pytest-forked-1.3.0.tar.gz", hash = "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca"}, + {file = "pytest_forked-1.3.0-py2.py3-none-any.whl", hash = "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"}, +] +pytest-xdist = [ + {file = "pytest-xdist-2.2.1.tar.gz", hash = "sha256:718887296892f92683f6a51f25a3ae584993b06f7076ce1e1fd482e59a8220a2"}, + {file = "pytest_xdist-2.2.1-py3-none-any.whl", hash = "sha256:2447a1592ab41745955fb870ac7023026f20a5f0bfccf1b52a879bd193d46450"}, +] python-dateutil = [ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, @@ -1529,8 +1723,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, ] sortedcontainers = [ - {file = "sortedcontainers-2.3.0-py2.py3-none-any.whl", hash = "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f"}, - {file = "sortedcontainers-2.3.0.tar.gz", hash = "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1"}, + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] soupsieve = [ {file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"}, @@ -1554,12 +1748,12 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] urllib3 = [ - {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, - {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, + {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, + {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, ] virtualenv = [ - {file = "virtualenv-20.4.6-py2.py3-none-any.whl", hash = "sha256:307a555cf21e1550885c82120eccaf5acedf42978fd362d32ba8410f9593f543"}, - {file = "virtualenv-20.4.6.tar.gz", hash = "sha256:72cf267afc04bf9c86ec932329b7e94db6a0331ae9847576daaa7ca3c86b29a4"}, + {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"}, + {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"}, ] yarl = [ {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"}, diff --git a/pyproject.toml b/pyproject.toml index 320bf88cc..2c9181889 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,9 @@ pep8-naming = "~=0.9" pre-commit = "~=2.1" taskipy = "~=1.7.0" python-dotenv = "~=0.17.1" +pytest = "~=6.2.4" +pytest-cov = "~=2.12.1" +pytest-xdist = { version = "~=2.2.1", extras = ["psutil"] } [build-system] requires = ["poetry-core>=1.0.0"] @@ -58,6 +61,6 @@ lint = "pre-commit run --all-files" precommit = "pre-commit install" build = "docker build -t ghcr.io/python-discord/bot:latest -f Dockerfile ." push = "docker push ghcr.io/python-discord/bot:latest" -test = "coverage run -m unittest" +test = "pytest -n auto --cov-report= --cov bot " html = "coverage html" report = "coverage report" diff --git a/tests/README.md b/tests/README.md index 1a17c09bd..a757f96c6 100644 --- a/tests/README.md +++ b/tests/README.md @@ -11,10 +11,14 @@ We are using the following modules and packages for our unit tests: - [unittest](https://docs.python.org/3/library/unittest.html) (standard library) - [unittest.mock](https://docs.python.org/3/library/unittest.mock.html) (standard library) - [coverage.py](https://coverage.readthedocs.io/en/stable/) +- [pytest-cov](https://pytest-cov.readthedocs.io/en/latest/index.html) + +We also use the following package as a test runner: +- [pytest](https://docs.pytest.org/en/6.2.x/) To ensure the results you obtain on your personal machine are comparable to those generated in the CI, please make sure to run your tests with the virtual environment defined by our [Poetry Project](/pyproject.toml). To run your tests with `poetry`, we've provided two "scripts" shortcuts: -- `poetry run task test` will run `unittest` with `coverage.py` +- `poetry run task test` will run `pytest` with `pytest-cov`. - `poetry run task test path/to/test.py` will run a specific test. - `poetry run task report` will generate a coverage report of the tests you've run with `poetry run task test`. If you append the `-m` flag to this command, the report will include the lines and branches not covered by tests in addition to the test coverage report. -- cgit v1.2.3 From 37584a8b8774c04b4111c29d96f8d06b31c89d84 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Mon, 7 Jun 2021 05:58:57 +0300 Subject: Adds Fast-Test Task Signed-off-by: Hassan Abouelela --- pyproject.toml | 3 ++- tests/README.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/pyproject.toml b/pyproject.toml index 2c9181889..774fe075c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ lint = "pre-commit run --all-files" precommit = "pre-commit install" build = "docker build -t ghcr.io/python-discord/bot:latest -f Dockerfile ." push = "docker push ghcr.io/python-discord/bot:latest" -test = "pytest -n auto --cov-report= --cov bot " +fast-test = "pytest -n auto" +test = "pytest -n auto --cov-report= --cov bot" html = "coverage html" report = "coverage report" diff --git a/tests/README.md b/tests/README.md index a757f96c6..b5fba9611 100644 --- a/tests/README.md +++ b/tests/README.md @@ -18,6 +18,7 @@ We also use the following package as a test runner: To ensure the results you obtain on your personal machine are comparable to those generated in the CI, please make sure to run your tests with the virtual environment defined by our [Poetry Project](/pyproject.toml). To run your tests with `poetry`, we've provided two "scripts" shortcuts: +- `poetry run task fast-test` will run `pytest`. - `poetry run task test` will run `pytest` with `pytest-cov`. - `poetry run task test path/to/test.py` will run a specific test. - `poetry run task report` will generate a coverage report of the tests you've run with `poetry run task test`. If you append the `-m` flag to this command, the report will include the lines and branches not covered by tests in addition to the test coverage report. -- cgit v1.2.3 From e45843ebe0a199d30b09ec7a0dcffc1ed9d4d9d7 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Mon, 7 Jun 2021 22:33:15 +0300 Subject: Fix Script Count In Documentation Signed-off-by: Hassan Abouelela --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/README.md b/tests/README.md index b5fba9611..339108951 100644 --- a/tests/README.md +++ b/tests/README.md @@ -16,7 +16,7 @@ We are using the following modules and packages for our unit tests: We also use the following package as a test runner: - [pytest](https://docs.pytest.org/en/6.2.x/) -To ensure the results you obtain on your personal machine are comparable to those generated in the CI, please make sure to run your tests with the virtual environment defined by our [Poetry Project](/pyproject.toml). To run your tests with `poetry`, we've provided two "scripts" shortcuts: +To ensure the results you obtain on your personal machine are comparable to those generated in the CI, please make sure to run your tests with the virtual environment defined by our [Poetry Project](/pyproject.toml). To run your tests with `poetry`, we've provided the following "script" shortcuts: - `poetry run task fast-test` will run `pytest`. - `poetry run task test` will run `pytest` with `pytest-cov`. -- cgit v1.2.3 From e75a46a4e8facec815ec374a12eaf400a404ee9c Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Thu, 10 Jun 2021 15:26:42 +0200 Subject: Tests: update infraction DM to mention UTC --- tests/bot/exts/moderation/infraction/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index ee9ff650c..50a717bb5 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -137,7 +137,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Ban", - expires="2020-02-26 09:20 (23 hours and 59 minutes)", + expires="2020-02-26 09:20 (23 hours and 59 minutes) UTC", reason="No reason provided." ), colour=Colours.soft_red, @@ -193,7 +193,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( type="Mute", - expires="2020-02-26 09:20 (23 hours and 59 minutes)", + expires="2020-02-26 09:20 (23 hours and 59 minutes) UTC", reason="Test" ), colour=Colours.soft_red, -- cgit v1.2.3 From 43659c7bac2e8127df402c97bbc3a26edf8256b6 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 12 Jun 2021 21:12:31 +0300 Subject: Renamed Test Task In Documentation Signed-off-by: Hassan Abouelela --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/README.md b/tests/README.md index 339108951..0192f916e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -18,7 +18,7 @@ We also use the following package as a test runner: To ensure the results you obtain on your personal machine are comparable to those generated in the CI, please make sure to run your tests with the virtual environment defined by our [Poetry Project](/pyproject.toml). To run your tests with `poetry`, we've provided the following "script" shortcuts: -- `poetry run task fast-test` will run `pytest`. +- `poetry run task test-nocov` will run `pytest`. - `poetry run task test` will run `pytest` with `pytest-cov`. - `poetry run task test path/to/test.py` will run a specific test. - `poetry run task report` will generate a coverage report of the tests you've run with `poetry run task test`. If you append the `-m` flag to this command, the report will include the lines and branches not covered by tests in addition to the test coverage report. -- cgit v1.2.3 From 089ef6219d98cbfcf9eed8bced6cb3ca43f0c55b Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Tue, 6 Jul 2021 02:33:57 +0300 Subject: Adds Documentation For Running A Single Test (#1669) Adds a portion to the testing README explaining how and when to run an individual test file when working with tests. Additionally adds a table of contents as the document has become quite long. Signed-off-by: Hassan Abouelela --- tests/README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) (limited to 'tests') diff --git a/tests/README.md b/tests/README.md index 0192f916e..b7fddfaa2 100644 --- a/tests/README.md +++ b/tests/README.md @@ -4,6 +4,14 @@ Our bot is one of the most important tools we have for running our community. As _**Note:** This is a practical guide to getting started with writing tests for our bot, not a general introduction to writing unit tests in Python. If you're looking for a more general introduction, you can take a look at the [Additional resources](#additional-resources) section at the bottom of this page._ +### Table of contents: +- [Tools](#tools) +- [Running tests](#running-tests) +- [Writing tests](#writing-tests) +- [Mocking](#mocking) +- [Some considerations](#some-considerations) +- [Additional resources](#additional-resources) + ## Tools We are using the following modules and packages for our unit tests: @@ -25,6 +33,29 @@ To ensure the results you obtain on your personal machine are comparable to thos If you want a coverage report, make sure to run the tests with `poetry run task test` *first*. +## Running tests +There are multiple ways to run the tests, which one you use will be determined by your goal, and stage in development. + +When actively developing, you'll most likely be working on one portion of the codebase, and as a result, won't need to run the entire test suite. +To run just one file, and save time, you can use the following command: +```shell +poetry run task test-nocov +``` + +For example: +```shell +poetry run task test-nocov tests/bot/exts/test_cogs.py +``` +will run the test suite in the `test_cogs` file. + +If you'd like to collect coverage as well, you can append `--cov` to the command above. + + +If you're done and are preparing to commit and push your code, it's a good idea to run the entire test suite as a sanity check: +```shell +poetry run task test +``` + ## Writing tests Since consistency is an important consideration for collaborative projects, we have written some guidelines on writing tests for the bot. In addition to these guidelines, it's a good idea to look at the existing code base for examples (e.g., [`test_converters.py`](/tests/bot/test_converters.py)). -- cgit v1.2.3 From 2a5a15f69d8ea3079f60e0e5d44387bc59061de5 Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Mon, 5 Jul 2021 19:48:29 -0400 Subject: chore: Update tests for the new codejam create command --- bot/exts/utils/jams.py | 1 - tests/bot/exts/utils/test_jams.py | 137 +++++++++++++++++++------------------- tests/helpers.py | 22 ++++++ 3 files changed, 92 insertions(+), 68 deletions(-) (limited to 'tests') diff --git a/bot/exts/utils/jams.py b/bot/exts/utils/jams.py index d45f9b57f..0fc84c2eb 100644 --- a/bot/exts/utils/jams.py +++ b/bot/exts/utils/jams.py @@ -79,7 +79,6 @@ class CodeJams(commands.Cog): If all categories are full or none exist, create a new category. """ for category in guild.categories: - # Need 2 available spaces: one for the text channel and one for voice. if category.name == CATEGORY_NAME and len(category.channels) < MAX_CHANNELS: return category diff --git a/tests/bot/exts/utils/test_jams.py b/tests/bot/exts/utils/test_jams.py index 85d6a1173..368a15476 100644 --- a/tests/bot/exts/utils/test_jams.py +++ b/tests/bot/exts/utils/test_jams.py @@ -2,10 +2,24 @@ import unittest from unittest.mock import AsyncMock, MagicMock, create_autospec from discord import CategoryChannel +from discord.ext.commands import BadArgument from bot.constants import Roles from bot.exts.utils import jams -from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockTextChannel +from tests.helpers import ( + MockAttachment, MockBot, MockCategoryChannel, MockContext, + MockGuild, MockMember, MockRole, MockTextChannel +) + +TEST_CSV = b"""\ +Team Name,Team Member Discord ID,Team Leader +Annoyed Alligators,12345,Y +Annoyed Alligators,54321,N +Oscillating Otters,12358,Y +Oscillating Otters,74832,N +Oscillating Otters,19903,N +Annoyed Alligators,11111,N +""" def get_mock_category(channel_count: int, name: str) -> CategoryChannel: @@ -17,8 +31,8 @@ def get_mock_category(channel_count: int, name: str) -> CategoryChannel: return category -class JamCreateTeamTests(unittest.IsolatedAsyncioTestCase): - """Tests for `createteam` command.""" +class JamCodejamCreateTests(unittest.IsolatedAsyncioTestCase): + """Tests for `codejam create` command.""" def setUp(self): self.bot = MockBot() @@ -28,60 +42,64 @@ class JamCreateTeamTests(unittest.IsolatedAsyncioTestCase): self.ctx = MockContext(bot=self.bot, author=self.command_user, guild=self.guild) self.cog = jams.CodeJams(self.bot) - async def test_too_small_amount_of_team_members_passed(self): - """Should `ctx.send` and exit early when too small amount of members.""" - for case in (1, 2): - with self.subTest(amount_of_members=case): - self.cog.create_channels = AsyncMock() - self.cog.add_roles = AsyncMock() + async def test_message_without_attachments(self): + """If no link or attachments are provided, commands.BadArgument should be raised.""" + self.ctx.message.attachments = [] - self.ctx.reset_mock() - members = (MockMember() for _ in range(case)) - await self.cog.createteam(self.cog, self.ctx, "foo", members) + with self.assertRaises(BadArgument): + await self.cog.create(self.cog, self.ctx, None) - self.ctx.send.assert_awaited_once() - self.cog.create_channels.assert_not_awaited() - self.cog.add_roles.assert_not_awaited() + async def test_result_sending(self): + """Should call `ctx.send` when everything goes right.""" + self.ctx.message.attachments = [MockAttachment()] + self.ctx.message.attachments[0].read = AsyncMock() + self.ctx.message.attachments[0].read.return_value = TEST_CSV + + team_leaders = MockRole() + + self.guild.get_member.return_value = MockMember() - async def test_duplicate_members_provided(self): - """Should `ctx.send` and exit early because duplicate members provided and total there is only 1 member.""" - self.cog.create_channels = AsyncMock() + self.ctx.guild.create_role = AsyncMock() + self.ctx.guild.create_role.return_value = team_leaders + self.cog.create_team_channel = AsyncMock() + self.cog.create_team_leader_channel = AsyncMock() self.cog.add_roles = AsyncMock() - member = MockMember() - await self.cog.createteam(self.cog, self.ctx, "foo", (member for _ in range(5))) + await self.cog.create(self.cog, self.ctx, None) + self.cog.create_team_channel.assert_awaited() + self.cog.create_team_leader_channel.assert_awaited_once_with( + self.ctx.guild, team_leaders + ) self.ctx.send.assert_awaited_once() - self.cog.create_channels.assert_not_awaited() - self.cog.add_roles.assert_not_awaited() - - async def test_result_sending(self): - """Should call `ctx.send` when everything goes right.""" - self.cog.create_channels = AsyncMock() - self.cog.add_roles = AsyncMock() - members = [MockMember() for _ in range(5)] - await self.cog.createteam(self.cog, self.ctx, "foo", members) + async def test_link_returning_non_200_status(self): + """When the URL passed returns a non 200 status, it should send a message informing them.""" + self.bot.http_session.get.return_value = mock = MagicMock() + mock.status = 404 + await self.cog.create(self.cog, self.ctx, "https://not-a-real-link.com") - self.cog.create_channels.assert_awaited_once() - self.cog.add_roles.assert_awaited_once() self.ctx.send.assert_awaited_once() async def test_category_doesnt_exist(self): """Should create a new code jam category.""" subtests = ( [], - [get_mock_category(jams.MAX_CHANNELS - 1, jams.CATEGORY_NAME)], + [get_mock_category(jams.MAX_CHANNELS, jams.CATEGORY_NAME)], [get_mock_category(jams.MAX_CHANNELS - 2, "other")], ) + self.cog.send_status_update = AsyncMock() + for categories in subtests: + self.cog.send_status_update.reset_mock() self.guild.reset_mock() self.guild.categories = categories with self.subTest(categories=categories): actual_category = await self.cog.get_category(self.guild) + self.cog.send_status_update.assert_called_once() self.guild.create_category_channel.assert_awaited_once() category_overwrites = self.guild.create_category_channel.call_args[1]["overwrites"] @@ -103,62 +121,47 @@ class JamCreateTeamTests(unittest.IsolatedAsyncioTestCase): async def test_channel_overwrites(self): """Should have correct permission overwrites for users and roles.""" - leader = MockMember() - members = [leader] + [MockMember() for _ in range(4)] + leader = (MockMember(), True) + members = [leader] + [(MockMember(), False) for _ in range(4)] overwrites = self.cog.get_overwrites(members, self.guild) - # Leader permission overwrites - self.assertTrue(overwrites[leader].manage_messages) - self.assertTrue(overwrites[leader].read_messages) - self.assertTrue(overwrites[leader].manage_webhooks) - self.assertTrue(overwrites[leader].connect) - - # Other members permission overwrites - for member in members[1:]: + for member, _ in members: self.assertTrue(overwrites[member].read_messages) - self.assertTrue(overwrites[member].connect) - - # Everyone role overwrite - self.assertFalse(overwrites[self.guild.default_role].read_messages) - self.assertFalse(overwrites[self.guild.default_role].connect) async def test_team_channels_creation(self): - """Should create new voice and text channel for team.""" - members = [MockMember() for _ in range(5)] + """Should create a text channel for a team.""" + team_leaders = MockRole() + members = [(MockMember(), True)] + [(MockMember(), False) for _ in range(5)] + category = MockCategoryChannel() + category.create_text_channel = AsyncMock() self.cog.get_overwrites = MagicMock() self.cog.get_category = AsyncMock() - self.ctx.guild.create_text_channel.return_value = MockTextChannel(mention="foobar-channel") - actual = await self.cog.create_channels(self.guild, "my-team", members) + self.cog.get_category.return_value = category + self.cog.add_team_leader_roles = AsyncMock() - self.assertEqual("foobar-channel", actual) + await self.cog.create_team_channel(self.guild, "my-team", members, team_leaders) + self.cog.add_team_leader_roles.assert_awaited_once_with(members, team_leaders) self.cog.get_overwrites.assert_called_once_with(members, self.guild) self.cog.get_category.assert_awaited_once_with(self.guild) - self.guild.create_text_channel.assert_awaited_once_with( + category.create_text_channel.assert_awaited_once_with( "my-team", - overwrites=self.cog.get_overwrites.return_value, - category=self.cog.get_category.return_value - ) - self.guild.create_voice_channel.assert_awaited_once_with( - "My Team", - overwrites=self.cog.get_overwrites.return_value, - category=self.cog.get_category.return_value + overwrites=self.cog.get_overwrites.return_value ) async def test_jam_roles_adding(self): """Should add team leader role to leader and jam role to every team member.""" leader_role = MockRole(name="Team Leader") - jam_role = MockRole(name="Jammer") - self.guild.get_role.side_effect = [leader_role, jam_role] leader = MockMember() - members = [leader] + [MockMember() for _ in range(4)] - await self.cog.add_roles(self.guild, members) + members = [(leader, True)] + [(MockMember(), False) for _ in range(4)] + await self.cog.add_team_leader_roles(members, leader_role) - leader.add_roles.assert_any_await(leader_role) - for member in members: - member.add_roles.assert_any_await(jam_role) + leader.add_roles.assert_awaited_once_with(leader_role) + for member, is_leader in members: + if not is_leader: + member.add_roles.assert_not_awaited() class CodeJamSetup(unittest.TestCase): diff --git a/tests/helpers.py b/tests/helpers.py index e3dc5fe5b..eedd7a601 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -361,6 +361,27 @@ class MockDMChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): super().__init__(**collections.ChainMap(kwargs, default_kwargs)) +# Create CategoryChannel instance to get a realistic MagicMock of `discord.CategoryChannel` +category_channel_data = { + 'id': 1, + 'type': discord.ChannelType.category, + 'name': 'category', + 'position': 1, +} + +state = unittest.mock.MagicMock() +guild = unittest.mock.MagicMock() +category_channel_instance = discord.CategoryChannel( + state=state, guild=guild, data=category_channel_data +) + + +class MockCategoryChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): + def __init__(self, **kwargs) -> None: + default_kwargs = {'id': next(self.discord_id)} + super().__init__(**collections.ChainMap(default_kwargs, kwargs)) + + # Create a Message instance to get a realistic MagicMock of `discord.Message` message_data = { 'id': 1, @@ -403,6 +424,7 @@ class MockContext(CustomMockMixin, unittest.mock.MagicMock): self.guild = kwargs.get('guild', MockGuild()) self.author = kwargs.get('author', MockMember()) self.channel = kwargs.get('channel', MockTextChannel()) + self.message = kwargs.get('message', MockMessage()) self.invoked_from_error_handler = kwargs.get('invoked_from_error_handler', False) -- cgit v1.2.3 From 2ff595670b0603fe9e97d16ffcc2c04457fea1c5 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Wed, 7 Jul 2021 18:21:06 +0300 Subject: Fixes N818 Compliance Renames a couple exceptions to include the error suffix, as enforced by N818. Signed-off-by: Hassan Abouelela --- bot/errors.py | 2 +- bot/exts/backend/error_handler.py | 4 ++-- bot/exts/moderation/infraction/_utils.py | 4 ++-- bot/pagination.py | 4 ++-- tests/bot/exts/backend/test_error_handler.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) (limited to 'tests') diff --git a/bot/errors.py b/bot/errors.py index 3544c6320..46efb6d4f 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -22,7 +22,7 @@ class LockedResourceError(RuntimeError): ) -class InvalidInfractedUser(Exception): +class InvalidInfractedUserError(Exception): """ Exception raised upon attempt of infracting an invalid user. diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index d8de177f5..7ef55af45 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -10,7 +10,7 @@ from bot.api import ResponseCodeError from bot.bot import Bot from bot.constants import Colours, Icons, MODERATION_ROLES from bot.converters import TagNameConverter -from bot.errors import InvalidInfractedUser, LockedResourceError +from bot.errors import InvalidInfractedUserError, LockedResourceError from bot.utils.checks import ContextCheckFailure log = logging.getLogger(__name__) @@ -76,7 +76,7 @@ class ErrorHandler(Cog): await self.handle_api_error(ctx, e.original) elif isinstance(e.original, LockedResourceError): await ctx.send(f"{e.original} Please wait for it to finish and try again later.") - elif isinstance(e.original, InvalidInfractedUser): + elif isinstance(e.original, InvalidInfractedUserError): await ctx.send(f"Cannot infract that user. {e.original.reason}") else: await self.handle_unexpected_error(ctx, e.original) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index e4eb7f79c..adbc641fa 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -7,7 +7,7 @@ from discord.ext.commands import Context from bot.api import ResponseCodeError from bot.constants import Colours, Icons -from bot.errors import InvalidInfractedUser +from bot.errors import InvalidInfractedUserError log = logging.getLogger(__name__) @@ -85,7 +85,7 @@ async def post_infraction( """Posts an infraction to the API.""" if isinstance(user, (discord.Member, discord.User)) and user.bot: log.trace(f"Posting of {infr_type} infraction for {user} to the API aborted. User is a bot.") - raise InvalidInfractedUser(user) + raise InvalidInfractedUserError(user) log.trace(f"Posting {infr_type} infraction for {user} to the API.") diff --git a/bot/pagination.py b/bot/pagination.py index 1c5b94b07..fbab74021 100644 --- a/bot/pagination.py +++ b/bot/pagination.py @@ -22,7 +22,7 @@ PAGINATION_EMOJI = (FIRST_EMOJI, LEFT_EMOJI, RIGHT_EMOJI, LAST_EMOJI, DELETE_EMO log = logging.getLogger(__name__) -class EmptyPaginatorEmbed(Exception): +class EmptyPaginatorEmbedError(Exception): """Raised when attempting to paginate with empty contents.""" pass @@ -233,7 +233,7 @@ class LinePaginator(Paginator): if not lines: if exception_on_empty_embed: log.exception("Pagination asked for empty lines iterable") - raise EmptyPaginatorEmbed("No lines to paginate") + raise EmptyPaginatorEmbedError("No lines to paginate") log.debug("No lines to add to paginator, adding '(nothing to display)' message") lines.append("(nothing to display)") diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index bd4fb5942..944cef6ca 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, call, patch from discord.ext.commands import errors from bot.api import ResponseCodeError -from bot.errors import InvalidInfractedUser, LockedResourceError +from bot.errors import InvalidInfractedUserError, LockedResourceError from bot.exts.backend.error_handler import ErrorHandler, setup from bot.exts.info.tags import Tags from bot.exts.moderation.silence import Silence @@ -130,7 +130,7 @@ class ErrorHandlerTests(unittest.IsolatedAsyncioTestCase): "expect_mock_call": "send" }, { - "args": (self.ctx, errors.CommandInvokeError(InvalidInfractedUser(self.ctx.author))), + "args": (self.ctx, errors.CommandInvokeError(InvalidInfractedUserError(self.ctx.author))), "expect_mock_call": "send" } ) -- cgit v1.2.3 From 5bbd29c8957c320ef94beacd191239032eb959ec Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 9 Jul 2021 03:32:51 +0300 Subject: Properly Handle Fuzzy Matching Help Fixes a bug where calling help with an invalid command would crash out during fuzzy matching. Signed-off-by: Hassan Abouelela --- bot/exts/info/help.py | 4 ++-- tests/bot/exts/info/test_help.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/bot/exts/info/test_help.py (limited to 'tests') diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index bf9ea5986..0235bbaf3 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -125,9 +125,9 @@ class CustomHelpCommand(HelpCommand): Will return an instance of the `HelpQueryNotFound` exception with the error message and possible matches. """ - choices = await self.get_all_help_choices() + choices = list(await self.get_all_help_choices()) result = process.extract(default_process(string), choices, scorer=fuzz.ratio, score_cutoff=60, processor=None) - return HelpQueryNotFound(f'Query "{string}" not found.', dict(result)) + return HelpQueryNotFound(f'Query "{string}" not found.', {choice[0]: choice[1] for choice in result}) async def subcommand_not_found(self, command: Command, string: str) -> "HelpQueryNotFound": """ diff --git a/tests/bot/exts/info/test_help.py b/tests/bot/exts/info/test_help.py new file mode 100644 index 000000000..604c69671 --- /dev/null +++ b/tests/bot/exts/info/test_help.py @@ -0,0 +1,23 @@ +import unittest + +import rapidfuzz + +from bot.exts.info import help +from tests.helpers import MockBot, MockContext, autospec + + +class HelpCogTests(unittest.IsolatedAsyncioTestCase): + def setUp(self) -> None: + """Attach an instance of the cog to the class for tests.""" + self.bot = MockBot() + self.cog = help.Help(self.bot) + self.ctx = MockContext(bot=self.bot) + self.bot.help_command.context = self.ctx + + @autospec(help.CustomHelpCommand, "get_all_help_choices", return_value={"help"}, pass_mocks=False) + async def test_help_fuzzy_matching(self): + """Test fuzzy matching of commands when called from help.""" + result = await self.bot.help_command.command_not_found("holp") + + match = {"help": rapidfuzz.fuzz.ratio("help", "holp")} + self.assertEqual(match, result.possible_matches) -- cgit v1.2.3