From 0a6415068b782d511cafa32002922061a61b9f67 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 16 Aug 2020 11:43:33 -0700 Subject: Silence: rename _get_instance_vars to _init_cog It's a more accurate name since it also reschedules unsilences now. --- tests/bot/cogs/moderation/test_silence.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index ab3d0742a..7c6efbfe4 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -83,19 +83,19 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_instance_vars_got_guild(self): """Bot got guild after it became available.""" - await self.cog._get_instance_vars() + await self.cog._init_cog() self.bot.wait_until_guild_available.assert_called_once() self.bot.get_guild.assert_called_once_with(Guild.id) async def test_instance_vars_got_role(self): """Got `Roles.verified` role from guild.""" - await self.cog._get_instance_vars() + await self.cog._init_cog() guild = self.bot.get_guild() guild.get_role.assert_called_once_with(Roles.verified) async def test_instance_vars_got_channels(self): """Got channels from bot.""" - await self.cog._get_instance_vars() + await self.cog._init_cog() self.bot.get_channel.called_once_with(Channels.mod_alerts) self.bot.get_channel.called_once_with(Channels.mod_log) @@ -104,7 +104,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): """Notifier was started with channel.""" mod_log = MockTextChannel() self.bot.get_channel.side_effect = (None, mod_log) - await self.cog._get_instance_vars() + await self.cog._init_cog() notifier.assert_called_once_with(mod_log) self.bot.get_channel.side_effect = None -- cgit v1.2.3 From 2300b66c09ac853fbf332ad1bbdd291d6d0c1d87 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 16 Aug 2020 12:27:24 -0700 Subject: Tests: optionally prevent autospec helper from passing mocks Not everything that's decorated needs the mocks that are patched. Being required to add the args to the test function anyway is annoying. It's especially bad if trying to decorate an entire test suite, as every test would need the args. Move the definition to a separate module to keep things cleaner. --- tests/_autospec.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/helpers.py | 21 ++---------------- 2 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 tests/_autospec.py (limited to 'tests') diff --git a/tests/_autospec.py b/tests/_autospec.py new file mode 100644 index 000000000..ee2fc1973 --- /dev/null +++ b/tests/_autospec.py @@ -0,0 +1,64 @@ +import contextlib +import functools +import unittest.mock +from typing import Callable + + +@functools.wraps(unittest.mock._patch.decoration_helper) +@contextlib.contextmanager +def _decoration_helper(self, patched, args, keywargs): + """Skips adding patchings as args if their `dont_pass` attribute is True.""" + # Don't ask what this does. It's just a copy from stdlib, but with the dont_pass check added. + extra_args = [] + with contextlib.ExitStack() as exit_stack: + for patching in patched.patchings: + arg = exit_stack.enter_context(patching) + if not getattr(patching, "dont_pass", False): + # Only add the patching as an arg if dont_pass is False. + if patching.attribute_name is not None: + keywargs.update(arg) + elif patching.new is unittest.mock.DEFAULT: + extra_args.append(arg) + + args += tuple(extra_args) + yield args, keywargs + + +@functools.wraps(unittest.mock._patch.copy) +def _copy(self): + """Copy the `dont_pass` attribute along with the standard copy operation.""" + patcher_copy = _copy.original(self) + patcher_copy.dont_pass = getattr(self, "dont_pass", False) + return patcher_copy + + +# Monkey-patch the patcher class :) +_copy.original = unittest.mock._patch.copy +unittest.mock._patch.copy = _copy +unittest.mock._patch.decoration_helper = _decoration_helper + + +def autospec(target, *attributes: str, pass_mocks: bool = True, **patch_kwargs) -> Callable: + """ + Patch multiple `attributes` of a `target` with autospecced mocks and `spec_set` as True. + + If `pass_mocks` is True, pass the autospecced mocks as arguments to the decorated object. + """ + # Caller's kwargs should take priority and overwrite the defaults. + kwargs = dict(spec_set=True, autospec=True) + kwargs.update(patch_kwargs) + + # Import the target if it's a string. + # This is to support both object and string targets like patch.multiple. + if type(target) is str: + target = unittest.mock._importer(target) + + def decorator(func): + for attribute in attributes: + patcher = unittest.mock.patch.object(target, attribute, **kwargs) + if not pass_mocks: + # A custom attribute to keep track of which patchings should be skipped. + patcher.dont_pass = True + func = patcher(func) + return func + return decorator diff --git a/tests/helpers.py b/tests/helpers.py index facc4e1af..6cf5d12bd 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -5,7 +5,7 @@ import itertools import logging import unittest.mock from asyncio import AbstractEventLoop -from typing import Callable, Iterable, Optional +from typing import Iterable, Optional import discord from aiohttp import ClientSession @@ -14,6 +14,7 @@ from discord.ext.commands import Context from bot.api import APIClient 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(): @@ -26,24 +27,6 @@ for logger in logging.Logger.manager.loggerDict.values(): logger.setLevel(logging.CRITICAL) -def autospec(target, *attributes: str, **kwargs) -> Callable: - """Patch multiple `attributes` of a `target` with autospecced mocks and `spec_set` as True.""" - # Caller's kwargs should take priority and overwrite the defaults. - kwargs = {'spec_set': True, 'autospec': True, **kwargs} - - # Import the target if it's a string. - # This is to support both object and string targets like patch.multiple. - if type(target) is str: - target = unittest.mock._importer(target) - - def decorator(func): - for attribute in attributes: - patcher = unittest.mock.patch.object(target, attribute, **kwargs) - func = patcher(func) - return func - return decorator - - class HashableMixin(discord.mixins.EqualityComparable): """ Mixin that provides similar hashing and equality functionality as discord.py's `Hashable` mixin. -- cgit v1.2.3 From 0ffa80717d9db89ab6ed8865741c39820d33b392 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 16 Aug 2020 12:11:21 -0700 Subject: Silence tests: mock RedisCaches --- tests/bot/cogs/moderation/test_silence.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 7c6efbfe4..8dfebb95c 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -6,7 +6,7 @@ from discord import PermissionOverwrite from bot.cogs.moderation.silence import Silence, SilenceNotifier from bot.constants import Channels, Emojis, Guild, Roles -from tests.helpers import MockBot, MockContext, MockTextChannel +from tests.helpers import MockBot, MockContext, MockTextChannel, autospec class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): @@ -72,14 +72,13 @@ class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): self.alert_channel.send.assert_not_called() +@autospec(Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) class SilenceTests(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.bot = MockBot() self.cog = Silence(self.bot) self.ctx = MockContext() self.cog._verified_role = None - # Set event so command callbacks can continue. - self.cog._get_instance_vars_event.set() async def test_instance_vars_got_guild(self): """Bot got guild after it became available.""" -- cgit v1.2.3 From 19f801289b14740017687a72d106875276507a1c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 16 Aug 2020 12:12:34 -0700 Subject: Silence tests: rename test_instance_vars to test_init_cog --- tests/bot/cogs/moderation/test_silence.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 8dfebb95c..67a61382c 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -80,26 +80,26 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.ctx = MockContext() self.cog._verified_role = None - async def test_instance_vars_got_guild(self): + async def test_init_cog_got_guild(self): """Bot got guild after it became available.""" await self.cog._init_cog() - self.bot.wait_until_guild_available.assert_called_once() + self.bot.wait_until_guild_available.assert_awaited_once() self.bot.get_guild.assert_called_once_with(Guild.id) - async def test_instance_vars_got_role(self): + async def test_init_cog_got_role(self): """Got `Roles.verified` role from guild.""" await self.cog._init_cog() guild = self.bot.get_guild() guild.get_role.assert_called_once_with(Roles.verified) - async def test_instance_vars_got_channels(self): + async def test_init_cog_got_channels(self): """Got channels from bot.""" await self.cog._init_cog() self.bot.get_channel.called_once_with(Channels.mod_alerts) self.bot.get_channel.called_once_with(Channels.mod_log) @mock.patch("bot.cogs.moderation.silence.SilenceNotifier") - async def test_instance_vars_got_notifier(self, notifier): + async def test_init_cog_got_notifier(self, notifier): """Notifier was started with channel.""" mod_log = MockTextChannel() self.bot.get_channel.side_effect = (None, mod_log) -- cgit v1.2.3 From de92d8553de25cc1ec45a5e4dbee8fd4d3426fa8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 16 Aug 2020 12:15:02 -0700 Subject: Silence tests: replace obsolete cog_unload tests Moderation notifications are no longer sent so that doesn't need to be tested. --- tests/bot/cogs/moderation/test_silence.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 67a61382c..807bb09a0 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -74,6 +74,7 @@ class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): @autospec(Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) class SilenceTests(unittest.IsolatedAsyncioTestCase): + @autospec("bot.cogs.moderation.silence", "Scheduler", pass_mocks=False) def setUp(self) -> None: self.bot = MockBot() self.cog = Silence(self.bot) @@ -237,20 +238,10 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): del mock_permissions_dict['send_messages'] self.assertDictEqual(mock_permissions_dict, new_permissions) - @mock.patch("bot.cogs.moderation.silence.asyncio") - @mock.patch.object(Silence, "_mod_alerts_channel", create=True) - def test_cog_unload_starts_task(self, alert_channel, asyncio_mock): - """Task for sending an alert was created with present `muted_channels`.""" - with mock.patch.object(self.cog, "muted_channels"): - self.cog.cog_unload() - alert_channel.send.assert_called_once_with(f"<@&{Roles.moderators}> channels left silenced on cog unload: ") - asyncio_mock.create_task.assert_called_once_with(alert_channel.send()) - - @mock.patch("bot.cogs.moderation.silence.asyncio") - def test_cog_unload_skips_task_start(self, asyncio_mock): - """No task created with no channels.""" + def test_cog_unload_cancels_tasks(self): + """All scheduled tasks should be cancelled.""" self.cog.cog_unload() - asyncio_mock.create_task.assert_not_called() + self.cog.scheduler.cancel_all.assert_called_once_with() @mock.patch("bot.cogs.moderation.silence.with_role_check") @mock.patch("bot.cogs.moderation.silence.MODERATION_ROLES", new=(1, 2, 3)) -- cgit v1.2.3 From 3182bacc342fd87313ffb22742947576d887f73b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 13:18:58 -0700 Subject: Silence tests: fix silence cache test for overwrites --- tests/bot/cogs/moderation/test_silence.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 807bb09a0..6f0cd880d 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -184,12 +184,15 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._silence(channel, False, None) self.cog.notifier.add_channel.assert_not_called() - async def test_silence_private_added_muted_channel(self): - """Channel was added to `muted_channels` on silence.""" + async def test_silence_private_cached_perms(self): + """Channel's previous overwrites were cached when silenced.""" channel = MockTextChannel() - with mock.patch.object(self.cog, "muted_channels") as muted_channels: - await self.cog._silence(channel, False, None) - muted_channels.add.assert_called_once_with(channel) + overwrite = PermissionOverwrite(send_messages=True, add_reactions=None) + overwrite_json = '{"send_messages": true, "add_reactions": null}' + channel.overwrites_for.return_value = overwrite + + await self.cog._silence(channel, False, None) + self.cog.muted_channel_perms.set.assert_called_once_with(channel.id, overwrite_json) async def test_unsilence_private_for_false(self): """Permissions are not set and `False` is returned in an unsilenced channel.""" -- cgit v1.2.3 From ccbc515cfedd5938f4d1d50404a1b32d66bd5511 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 13:29:16 -0700 Subject: Silence tests: fix test_unsilence_private_for_false --- tests/bot/cogs/moderation/test_silence.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 6f0cd880d..c1039f85c 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -196,7 +196,10 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_unsilence_private_for_false(self): """Permissions are not set and `False` is returned in an unsilenced channel.""" - channel = Mock() + self.cog.scheduler.__contains__.return_value = False + self.cog.muted_channel_perms.get.return_value = None + channel = MockTextChannel() + self.assertFalse(await self.cog._unsilence(channel)) channel.set_permissions.assert_not_called() -- cgit v1.2.3 From 28f7f66e4e9543d79f5a64ee6a0e0bbc80088804 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 13:33:47 -0700 Subject: Silence tests: fix test_silence_private_notifier --- tests/bot/cogs/moderation/test_silence.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index c1039f85c..e2e3ca9c1 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -174,6 +174,9 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_silence_private_notifier(self): """Channel should be added to notifier with `persistent` set to `True`, and the other way around.""" channel = MockTextChannel() + overwrite = PermissionOverwrite(send_messages=True, add_reactions=None) + channel.overwrites_for.return_value = overwrite + with mock.patch.object(self.cog, "notifier", create=True): with self.subTest(persistent=True): await self.cog._silence(channel, True, None) -- cgit v1.2.3 From c24fa4371c9e8b13431b5d5f3808401dda8d449a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 14:20:05 -0700 Subject: Silence tests: fix test_silence_private_silenced_channel --- tests/bot/cogs/moderation/test_silence.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index e2e3ca9c1..75b4ef470 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -151,11 +151,18 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): channel.set_permissions.assert_not_called() async def test_silence_private_silenced_channel(self): - """Channel had `send_message` permissions revoked.""" + """Channel had `send_message` and `add_reactions` permissions revoked for verified role.""" channel = MockTextChannel() + overwrite = PermissionOverwrite(send_messages=True, add_reactions=None) + channel.overwrites_for.return_value = overwrite + self.assertTrue(await self.cog._silence(channel, False, None)) - channel.set_permissions.assert_called_once() - self.assertFalse(channel.set_permissions.call_args.kwargs['send_messages']) + self.assertFalse(overwrite.send_messages) + self.assertFalse(overwrite.add_reactions) + channel.set_permissions.assert_awaited_once_with( + self.cog._verified_role, + overwrite=overwrite + ) async def test_silence_private_preserves_permissions(self): """Previous permissions were preserved when channel was silenced.""" -- cgit v1.2.3 From 830bb36654103474fa74505f3e0ff4bdf91656fd Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 14:28:02 -0700 Subject: Silence tests: fix test_silence_private_for_false --- tests/bot/cogs/moderation/test_silence.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 75b4ef470..5763c4cdd 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -144,11 +144,20 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_silence_private_for_false(self): """Permissions are not set and `False` is returned in an already silenced channel.""" - perm_overwrite = Mock(send_messages=False) - channel = Mock(overwrites_for=Mock(return_value=perm_overwrite)) + 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)), + ) - self.assertFalse(await self.cog._silence(channel, True, None)) - channel.set_permissions.assert_not_called() + for contains, overwrite in subtests: + with self.subTest(contains=contains, overwrite=overwrite): + self.cog.scheduler.__contains__.return_value = contains + channel = MockTextChannel() + channel.overwrites_for.return_value = overwrite + + self.assertFalse(await self.cog._silence(channel, True, None)) + channel.set_permissions.assert_not_called() async def test_silence_private_silenced_channel(self): """Channel had `send_message` and `add_reactions` permissions revoked for verified role.""" -- cgit v1.2.3 From f8485f17ac0263c47365893438b3f1da609cb259 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 14:47:28 -0700 Subject: Silence tests: fix command message tests --- tests/bot/cogs/moderation/test_silence.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 5763c4cdd..02964d7ab 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -115,15 +115,12 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): (None, f"{Emojis.check_mark} silenced current channel indefinitely.", True,), (5, f"{Emojis.cross_mark} current channel is already silenced.", False,), ) - for duration, result_message, _silence_patch_return in test_cases: - with self.subTest( - silence_duration=duration, - result_message=result_message, - starting_unsilenced_state=_silence_patch_return - ): - with mock.patch.object(self.cog, "_silence", return_value=_silence_patch_return): + for duration, message, was_silenced in test_cases: + self.cog._init_task = mock.AsyncMock()() + with self.subTest(was_silenced=was_silenced, message=message, duration=duration): + with mock.patch.object(self.cog, "_silence", return_value=was_silenced): await self.cog.silence.callback(self.cog, self.ctx, duration) - self.ctx.send.assert_called_once_with(result_message) + self.ctx.send.assert_called_once_with(message) self.ctx.reset_mock() async def test_unsilence_sent_correct_discord_message(self): @@ -132,14 +129,12 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): (True, f"{Emojis.check_mark} unsilenced current channel."), (False, f"{Emojis.cross_mark} current channel was not silenced.") ) - for _unsilence_patch_return, result_message in test_cases: - with self.subTest( - starting_silenced_state=_unsilence_patch_return, - result_message=result_message - ): - with mock.patch.object(self.cog, "_unsilence", return_value=_unsilence_patch_return): + for was_unsilenced, message in test_cases: + self.cog._init_task = mock.AsyncMock()() + with self.subTest(was_unsilenced=was_unsilenced, message=message): + with mock.patch.object(self.cog, "_unsilence", return_value=was_unsilenced): await self.cog.unsilence.callback(self.cog, self.ctx) - self.ctx.send.assert_called_once_with(result_message) + self.ctx.channel.send.assert_called_once_with(message) self.ctx.reset_mock() async def test_silence_private_for_false(self): -- cgit v1.2.3 From 89107eccca3213c436028b997bdc6785fa9ce02d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 15:09:31 -0700 Subject: Silence tests: fix overwrite preservation test for silences --- tests/bot/cogs/moderation/test_silence.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 02964d7ab..765c324d2 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -168,19 +168,23 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): overwrite=overwrite ) - async def test_silence_private_preserves_permissions(self): - """Previous permissions were preserved when channel was silenced.""" + async def test_silence_private_preserves_other_overwrites(self): + """Channel's other unrelated overwrites were not changed when it was silenced.""" channel = MockTextChannel() - # Set up mock channel permission state. - mock_permissions = PermissionOverwrite() - mock_permissions_dict = dict(mock_permissions) - channel.overwrites_for.return_value = mock_permissions + overwrite = PermissionOverwrite(stream=True, attach_files=False) + channel.overwrites_for.return_value = overwrite + + prev_overwrite_dict = dict(overwrite) await self.cog._silence(channel, False, None) - new_permissions = channel.set_permissions.call_args.kwargs - # Remove 'send_messages' key because it got changed in the method. - del new_permissions['send_messages'] - del mock_permissions_dict['send_messages'] - self.assertDictEqual(mock_permissions_dict, new_permissions) + new_overwrite_dict = dict(overwrite) + + # Remove 'send_messages' & 'add_reactions' keys because they were changed by the method. + del prev_overwrite_dict['send_messages'] + del prev_overwrite_dict['add_reactions'] + del new_overwrite_dict['send_messages'] + del new_overwrite_dict['add_reactions'] + + self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) async def test_silence_private_notifier(self): """Channel should be added to notifier with `persistent` set to `True`, and the other way around.""" -- cgit v1.2.3 From eff788aab84ee96408823eb61ebafba2d9e9cbcb Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 15:39:00 -0700 Subject: Silence tests: fix test_unsilence_private_removed_notifier --- tests/bot/cogs/moderation/test_silence.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 765c324d2..b21f5f61a 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -233,8 +233,11 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): @mock.patch.object(Silence, "notifier", create=True) async def test_unsilence_private_removed_notifier(self, notifier): """Channel was removed from `notifier` on unsilence.""" - perm_overwrite = MagicMock(send_messages=False) - channel = MockTextChannel(overwrites_for=Mock(return_value=perm_overwrite)) + overwrite_json = '{"send_messages": true, "add_reactions": null}' + self.cog.muted_channel_perms.get.return_value = overwrite_json + channel = MockTextChannel() + channel.overwrites_for.return_value = PermissionOverwrite() + await self.cog._unsilence(channel) notifier.remove_channel.assert_called_once_with(channel) -- cgit v1.2.3 From 3ec17dac3fe8e00f34489a5e3dd927bec39e91e6 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 15:58:31 -0700 Subject: Silence tests: fix tests for _unsilence Add a fixture to set up mocks for a successful `unsilence` call. This reduces code redundancy. --- tests/bot/cogs/moderation/test_silence.py | 75 ++++++++++++++++++------------- 1 file changed, 45 insertions(+), 30 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index b21f5f61a..ff32e9a05 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -1,6 +1,6 @@ import unittest from unittest import mock -from unittest.mock import MagicMock, Mock +from unittest.mock import Mock from discord import PermissionOverwrite @@ -81,6 +81,18 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.ctx = MockContext() self.cog._verified_role = None + def unsilence_fixture(self) -> MockTextChannel: + """Setup mocks for a successful `_unsilence` call. Return the mocked channel.""" + overwrite_json = '{"send_messages": true, "add_reactions": null}' + self.cog.muted_channel_perms.get.return_value = overwrite_json + + # stream=True just to have at least one other overwrite not be the default value. + channel = MockTextChannel() + overwrite = PermissionOverwrite(stream=True, send_messages=False, add_reactions=False) + channel.overwrites_for.return_value = overwrite + + return channel + async def test_init_cog_got_guild(self): """Bot got guild after it became available.""" await self.cog._init_cog() @@ -223,47 +235,50 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): @mock.patch.object(Silence, "notifier", create=True) async def test_unsilence_private_unsilenced_channel(self, _): - """Channel had `send_message` permissions restored""" - perm_overwrite = MagicMock(send_messages=False) - channel = MockTextChannel(overwrites_for=Mock(return_value=perm_overwrite)) - self.assertTrue(await self.cog._unsilence(channel)) - channel.set_permissions.assert_called_once() - self.assertIsNone(channel.set_permissions.call_args.kwargs['send_messages']) + """Channel had `send_message` permissions restored.""" + channel = self.unsilence_fixture() + overwrite = channel.overwrites_for.return_value + + await self.cog._unsilence(channel) + channel.set_permissions.assert_awaited_once_with( + self.cog._verified_role, overwrite=overwrite + ) + + # Recall that these values are determined by the fixture. + self.assertTrue(overwrite.send_messages) + self.assertIsNone(overwrite.add_reactions) @mock.patch.object(Silence, "notifier", create=True) async def test_unsilence_private_removed_notifier(self, notifier): """Channel was removed from `notifier` on unsilence.""" - overwrite_json = '{"send_messages": true, "add_reactions": null}' - self.cog.muted_channel_perms.get.return_value = overwrite_json - channel = MockTextChannel() - channel.overwrites_for.return_value = PermissionOverwrite() - + channel = self.unsilence_fixture() await self.cog._unsilence(channel) notifier.remove_channel.assert_called_once_with(channel) @mock.patch.object(Silence, "notifier", create=True) async def test_unsilence_private_removed_muted_channel(self, _): - """Channel was removed from `muted_channels` on unsilence.""" - perm_overwrite = MagicMock(send_messages=False) - channel = MockTextChannel(overwrites_for=Mock(return_value=perm_overwrite)) - with mock.patch.object(self.cog, "muted_channels") as muted_channels: - await self.cog._unsilence(channel) - muted_channels.discard.assert_called_once_with(channel) + """Channel was removed from overwrites cache on unsilence.""" + channel = self.unsilence_fixture() + await self.cog._unsilence(channel) + self.cog.muted_channel_perms.delete.assert_awaited_once_with(channel.id) @mock.patch.object(Silence, "notifier", create=True) - async def test_unsilence_private_preserves_permissions(self, _): - """Previous permissions were preserved when channel was unsilenced.""" - channel = MockTextChannel() - # Set up mock channel permission state. - mock_permissions = PermissionOverwrite(send_messages=False) - mock_permissions_dict = dict(mock_permissions) - channel.overwrites_for.return_value = mock_permissions + async def test_unsilence_private_preserves_other_overwrites(self, _): + """Channel's other unrelated overwrites were not changed when it was unsilenced.""" + channel = self.unsilence_fixture() + overwrite = channel.overwrites_for.return_value + + prev_overwrite_dict = dict(overwrite) await self.cog._unsilence(channel) - new_permissions = channel.set_permissions.call_args.kwargs - # Remove 'send_messages' key because it got changed in the method. - del new_permissions['send_messages'] - del mock_permissions_dict['send_messages'] - self.assertDictEqual(mock_permissions_dict, new_permissions) + new_overwrite_dict = dict(overwrite) + + # Remove 'send_messages' & 'add_reactions' keys because they were changed by the method. + del prev_overwrite_dict['send_messages'] + del prev_overwrite_dict['add_reactions'] + del new_overwrite_dict['send_messages'] + del new_overwrite_dict['add_reactions'] + + self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) def test_cog_unload_cancels_tasks(self): """All scheduled tasks should be cancelled.""" -- cgit v1.2.3 From 0adaf09af5c7a76566f874eda429dd6f263189be Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 16:33:39 -0700 Subject: Silence tests: separate test cases; refactor names & docstrings --- tests/bot/cogs/moderation/test_silence.py | 166 +++++++++++++++++------------- 1 file changed, 95 insertions(+), 71 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index ff32e9a05..f3ee184d4 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -1,3 +1,4 @@ +import asyncio import unittest from unittest import mock from unittest.mock import Mock @@ -73,25 +74,13 @@ class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): @autospec(Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) -class SilenceTests(unittest.IsolatedAsyncioTestCase): +class SilenceCogTests(unittest.IsolatedAsyncioTestCase): + """Tests for the general functionality of the Silence cog.""" + @autospec("bot.cogs.moderation.silence", "Scheduler", pass_mocks=False) def setUp(self) -> None: self.bot = MockBot() self.cog = Silence(self.bot) - self.ctx = MockContext() - self.cog._verified_role = None - - def unsilence_fixture(self) -> MockTextChannel: - """Setup mocks for a successful `_unsilence` call. Return the mocked channel.""" - overwrite_json = '{"send_messages": true, "add_reactions": null}' - self.cog.muted_channel_perms.get.return_value = overwrite_json - - # stream=True just to have at least one other overwrite not be the default value. - channel = MockTextChannel() - overwrite = PermissionOverwrite(stream=True, send_messages=False, add_reactions=False) - channel.overwrites_for.return_value = overwrite - - return channel async def test_init_cog_got_guild(self): """Bot got guild after it became available.""" @@ -120,37 +109,49 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): notifier.assert_called_once_with(mod_log) self.bot.get_channel.side_effect = None - async def test_silence_sent_correct_discord_message(self): - """Check if proper message was sent when called with duration in channel with previous state.""" + def test_cog_unload_cancelled_tasks(self): + """All scheduled tasks were cancelled.""" + self.cog.cog_unload() + self.cog.scheduler.cancel_all.assert_called_once_with() + + @mock.patch("bot.cogs.moderation.silence.with_role_check") + @mock.patch("bot.cogs.moderation.silence.MODERATION_ROLES", new=(1, 2, 3)) + def test_cog_check(self, role_check): + """Role check was called with `MODERATION_ROLES`""" + ctx = MockContext() + self.cog.cog_check(ctx) + role_check.assert_called_once_with(ctx, *(1, 2, 3)) + + +@autospec(Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) +class SilenceTests(unittest.IsolatedAsyncioTestCase): + """Tests for the silence command and its related helper methods.""" + + @autospec(Silence, "_reschedule", pass_mocks=False) + @autospec("bot.cogs.moderation.silence", "Scheduler", "SilenceNotifier", pass_mocks=False) + def setUp(self) -> None: + self.bot = MockBot() + self.cog = Silence(self.bot) + self.cog._init_task = mock.AsyncMock()() + + asyncio.run(self.cog._init_cog()) # Populate instance attributes. + + async def test_sent_correct_message(self): + """Appropriate failure/success message was sent by the command.""" test_cases = ( (0.0001, f"{Emojis.check_mark} silenced current channel for 0.0001 minute(s).", True,), (None, f"{Emojis.check_mark} silenced current channel indefinitely.", True,), (5, f"{Emojis.cross_mark} current channel is already silenced.", False,), ) for duration, message, was_silenced in test_cases: - self.cog._init_task = mock.AsyncMock()() + ctx = MockContext() with self.subTest(was_silenced=was_silenced, message=message, duration=duration): with mock.patch.object(self.cog, "_silence", return_value=was_silenced): - await self.cog.silence.callback(self.cog, self.ctx, duration) - self.ctx.send.assert_called_once_with(message) - self.ctx.reset_mock() + await self.cog.silence.callback(self.cog, ctx, duration) + ctx.send.assert_called_once_with(message) - async def test_unsilence_sent_correct_discord_message(self): - """Check if proper message was sent when unsilencing channel.""" - test_cases = ( - (True, f"{Emojis.check_mark} unsilenced current channel."), - (False, f"{Emojis.cross_mark} current channel was not silenced.") - ) - for was_unsilenced, message in test_cases: - self.cog._init_task = mock.AsyncMock()() - with self.subTest(was_unsilenced=was_unsilenced, message=message): - with mock.patch.object(self.cog, "_unsilence", return_value=was_unsilenced): - await self.cog.unsilence.callback(self.cog, self.ctx) - self.ctx.channel.send.assert_called_once_with(message) - self.ctx.reset_mock() - - async def test_silence_private_for_false(self): - """Permissions are not set and `False` is returned in an already silenced channel.""" + 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)), @@ -166,7 +167,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.assertFalse(await self.cog._silence(channel, True, None)) channel.set_permissions.assert_not_called() - async def test_silence_private_silenced_channel(self): + async def test_silenced_channel(self): """Channel had `send_message` and `add_reactions` permissions revoked for verified role.""" channel = MockTextChannel() overwrite = PermissionOverwrite(send_messages=True, add_reactions=None) @@ -180,8 +181,8 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): overwrite=overwrite ) - async def test_silence_private_preserves_other_overwrites(self): - """Channel's other unrelated overwrites were not changed when it was silenced.""" + async def test_preserved_other_overwrites(self): + """Channel's other unrelated overwrites were not changed.""" channel = MockTextChannel() overwrite = PermissionOverwrite(stream=True, attach_files=False) channel.overwrites_for.return_value = overwrite @@ -198,8 +199,8 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) - async def test_silence_private_notifier(self): - """Channel should be added to notifier with `persistent` set to `True`, and the other way around.""" + async def test_added_removed_notifier(self): + """Channel was added to notifier if `persistent` was `True`, and removed if `False`.""" channel = MockTextChannel() overwrite = PermissionOverwrite(send_messages=True, add_reactions=None) channel.overwrites_for.return_value = overwrite @@ -214,8 +215,8 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._silence(channel, False, None) self.cog.notifier.add_channel.assert_not_called() - async def test_silence_private_cached_perms(self): - """Channel's previous overwrites were cached when silenced.""" + async def test_cached_previous_overwrites(self): + """Channel's previous overwrites were cached.""" channel = MockTextChannel() overwrite = PermissionOverwrite(send_messages=True, add_reactions=None) overwrite_json = '{"send_messages": true, "add_reactions": null}' @@ -224,8 +225,47 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._silence(channel, False, None) self.cog.muted_channel_perms.set.assert_called_once_with(channel.id, overwrite_json) - async def test_unsilence_private_for_false(self): - """Permissions are not set and `False` is returned in an unsilenced channel.""" + +@autospec(Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) +class UnsilenceTests(unittest.IsolatedAsyncioTestCase): + """Tests for the unsilence command and its related helper methods.""" + + @autospec(Silence, "_reschedule", pass_mocks=False) + @autospec("bot.cogs.moderation.silence", "Scheduler", "SilenceNotifier", pass_mocks=False) + def setUp(self) -> None: + self.bot = MockBot() + self.cog = Silence(self.bot) + self.cog._init_task = mock.AsyncMock()() + + asyncio.run(self.cog._init_cog()) # Populate instance attributes. + + def unsilence_fixture(self) -> MockTextChannel: + """Setup mocks for a successful `_unsilence` call. Return the mocked channel.""" + overwrite_json = '{"send_messages": true, "add_reactions": null}' + self.cog.muted_channel_perms.get.return_value = overwrite_json + + # stream=True just to have at least one other overwrite not be the default value. + channel = MockTextChannel() + overwrite = PermissionOverwrite(stream=True, send_messages=False, add_reactions=False) + channel.overwrites_for.return_value = overwrite + + return channel + + async def test_sent_correct_message(self): + """Appropriate failure/success message was sent by the command.""" + test_cases = ( + (True, f"{Emojis.check_mark} unsilenced current channel."), + (False, f"{Emojis.cross_mark} current channel was not silenced.") + ) + for was_unsilenced, message in test_cases: + ctx = MockContext() + with self.subTest(was_unsilenced=was_unsilenced, message=message): + with mock.patch.object(self.cog, "_unsilence", return_value=was_unsilenced): + await self.cog.unsilence.callback(self.cog, ctx) + ctx.channel.send.assert_called_once_with(message) + + 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 self.cog.muted_channel_perms.get.return_value = None channel = MockTextChannel() @@ -233,9 +273,8 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.assertFalse(await self.cog._unsilence(channel)) channel.set_permissions.assert_not_called() - @mock.patch.object(Silence, "notifier", create=True) - async def test_unsilence_private_unsilenced_channel(self, _): - """Channel had `send_message` permissions restored.""" + async def test_unsilenced_channel(self): + """Channel's `send_message` and `add_reactions` overwrites were restored.""" channel = self.unsilence_fixture() overwrite = channel.overwrites_for.return_value @@ -248,23 +287,20 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.assertTrue(overwrite.send_messages) self.assertIsNone(overwrite.add_reactions) - @mock.patch.object(Silence, "notifier", create=True) - async def test_unsilence_private_removed_notifier(self, notifier): - """Channel was removed from `notifier` on unsilence.""" + async def test_removed_notifier(self): + """Channel was removed from `notifier`.""" channel = self.unsilence_fixture() await self.cog._unsilence(channel) - notifier.remove_channel.assert_called_once_with(channel) + self.cog.notifier.remove_channel.assert_called_once_with(channel) - @mock.patch.object(Silence, "notifier", create=True) - async def test_unsilence_private_removed_muted_channel(self, _): - """Channel was removed from overwrites cache on unsilence.""" + async def test_deleted_cached_overwrite(self): + """Channel was deleted from the overwrites cache.""" channel = self.unsilence_fixture() await self.cog._unsilence(channel) self.cog.muted_channel_perms.delete.assert_awaited_once_with(channel.id) - @mock.patch.object(Silence, "notifier", create=True) - async def test_unsilence_private_preserves_other_overwrites(self, _): - """Channel's other unrelated overwrites were not changed when it was unsilenced.""" + async def test_preserved_other_overwrites(self): + """Channel's other unrelated overwrites were not changed.""" channel = self.unsilence_fixture() overwrite = channel.overwrites_for.return_value @@ -279,15 +315,3 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): del new_overwrite_dict['add_reactions'] self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) - - def test_cog_unload_cancels_tasks(self): - """All scheduled tasks should be cancelled.""" - self.cog.cog_unload() - self.cog.scheduler.cancel_all.assert_called_once_with() - - @mock.patch("bot.cogs.moderation.silence.with_role_check") - @mock.patch("bot.cogs.moderation.silence.MODERATION_ROLES", new=(1, 2, 3)) - def test_cog_check(self, role_check): - """Role check is called with `MODERATION_ROLES`""" - self.cog.cog_check(self.ctx) - role_check.assert_called_once_with(self.ctx, *(1, 2, 3)) -- cgit v1.2.3 From 3f4d409bd951e749bdc643b0bc288cfc0f5136bd Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 16:46:13 -0700 Subject: Silence tests: autospec _reschedule and SilenceNotifier for cog tests --- tests/bot/cogs/moderation/test_silence.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index f3ee184d4..ae8b59ff5 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -82,39 +82,45 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): self.bot = MockBot() self.cog = Silence(self.bot) + @autospec(Silence, "_reschedule", pass_mocks=False) + @autospec("bot.cogs.moderation.silence", "SilenceNotifier", pass_mocks=False) async def test_init_cog_got_guild(self): """Bot got guild after it became available.""" await self.cog._init_cog() self.bot.wait_until_guild_available.assert_awaited_once() self.bot.get_guild.assert_called_once_with(Guild.id) + @autospec(Silence, "_reschedule", pass_mocks=False) + @autospec("bot.cogs.moderation.silence", "SilenceNotifier", pass_mocks=False) async def test_init_cog_got_role(self): """Got `Roles.verified` role from guild.""" await self.cog._init_cog() guild = self.bot.get_guild() guild.get_role.assert_called_once_with(Roles.verified) + @autospec(Silence, "_reschedule", pass_mocks=False) + @autospec("bot.cogs.moderation.silence", "SilenceNotifier", pass_mocks=False) async def test_init_cog_got_channels(self): """Got channels from bot.""" await self.cog._init_cog() self.bot.get_channel.called_once_with(Channels.mod_alerts) self.bot.get_channel.called_once_with(Channels.mod_log) - @mock.patch("bot.cogs.moderation.silence.SilenceNotifier") + @autospec(Silence, "_reschedule", pass_mocks=False) + @autospec("bot.cogs.moderation.silence", "SilenceNotifier") async def test_init_cog_got_notifier(self, notifier): """Notifier was started with channel.""" mod_log = MockTextChannel() self.bot.get_channel.side_effect = (None, mod_log) await self.cog._init_cog() - notifier.assert_called_once_with(mod_log) - self.bot.get_channel.side_effect = None + notifier.assert_called_once_with(self.cog._mod_log_channel) def test_cog_unload_cancelled_tasks(self): """All scheduled tasks were cancelled.""" self.cog.cog_unload() self.cog.scheduler.cancel_all.assert_called_once_with() - @mock.patch("bot.cogs.moderation.silence.with_role_check") + @autospec("bot.cogs.moderation.silence", "with_role_check") @mock.patch("bot.cogs.moderation.silence.MODERATION_ROLES", new=(1, 2, 3)) def test_cog_check(self, role_check): """Role check was called with `MODERATION_ROLES`""" -- cgit v1.2.3 From a13bf79ba2aa672e52bcffe38720c7686cac67ba Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 18:40:36 -0700 Subject: Silence tests: merge unsilence fixture into setUp Now that there are separate test cases, there's no need to keep the fixtures separate. --- tests/bot/cogs/moderation/test_silence.py | 52 ++++++++++++------------------- 1 file changed, 20 insertions(+), 32 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index ae8b59ff5..fe6045c87 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -232,7 +232,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.cog.muted_channel_perms.set.assert_called_once_with(channel.id, overwrite_json) -@autospec(Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) +@autospec(Silence, "muted_channel_times", pass_mocks=False) class UnsilenceTests(unittest.IsolatedAsyncioTestCase): """Tests for the unsilence command and its related helper methods.""" @@ -243,19 +243,15 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.cog = Silence(self.bot) self.cog._init_task = mock.AsyncMock()() - asyncio.run(self.cog._init_cog()) # Populate instance attributes. - - def unsilence_fixture(self) -> MockTextChannel: - """Setup mocks for a successful `_unsilence` call. Return the mocked channel.""" - overwrite_json = '{"send_messages": true, "add_reactions": null}' - self.cog.muted_channel_perms.get.return_value = overwrite_json + perms_cache = mock.create_autospec(self.cog.muted_channel_perms, spec_set=True) + self.cog.muted_channel_perms = perms_cache - # stream=True just to have at least one other overwrite not be the default value. - channel = MockTextChannel() - overwrite = PermissionOverwrite(stream=True, send_messages=False, add_reactions=False) - channel.overwrites_for.return_value = overwrite + asyncio.run(self.cog._init_cog()) # Populate instance attributes. - return channel + perms_cache.get.return_value = '{"send_messages": true, "add_reactions": null}' + self.channel = MockTextChannel() + self.overwrite = PermissionOverwrite(stream=True, send_messages=False, add_reactions=False) + self.channel.overwrites_for.return_value = self.overwrite async def test_sent_correct_message(self): """Appropriate failure/success message was sent by the command.""" @@ -281,38 +277,30 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): async def test_unsilenced_channel(self): """Channel's `send_message` and `add_reactions` overwrites were restored.""" - channel = self.unsilence_fixture() - overwrite = channel.overwrites_for.return_value - - await self.cog._unsilence(channel) - channel.set_permissions.assert_awaited_once_with( - self.cog._verified_role, overwrite=overwrite + await self.cog._unsilence(self.channel) + self.channel.set_permissions.assert_awaited_once_with( + self.cog._verified_role, overwrite=self.overwrite ) # Recall that these values are determined by the fixture. - self.assertTrue(overwrite.send_messages) - self.assertIsNone(overwrite.add_reactions) + self.assertTrue(self.overwrite.send_messages) + self.assertIsNone(self.overwrite.add_reactions) async def test_removed_notifier(self): """Channel was removed from `notifier`.""" - channel = self.unsilence_fixture() - await self.cog._unsilence(channel) - self.cog.notifier.remove_channel.assert_called_once_with(channel) + await self.cog._unsilence(self.channel) + self.cog.notifier.remove_channel.assert_called_once_with(self.channel) async def test_deleted_cached_overwrite(self): """Channel was deleted from the overwrites cache.""" - channel = self.unsilence_fixture() - await self.cog._unsilence(channel) - self.cog.muted_channel_perms.delete.assert_awaited_once_with(channel.id) + await self.cog._unsilence(self.channel) + self.cog.muted_channel_perms.delete.assert_awaited_once_with(self.channel.id) async def test_preserved_other_overwrites(self): """Channel's other unrelated overwrites were not changed.""" - channel = self.unsilence_fixture() - overwrite = channel.overwrites_for.return_value - - prev_overwrite_dict = dict(overwrite) - await self.cog._unsilence(channel) - new_overwrite_dict = dict(overwrite) + prev_overwrite_dict = dict(self.overwrite) + await self.cog._unsilence(self.channel) + new_overwrite_dict = dict(self.overwrite) # Remove 'send_messages' & 'add_reactions' keys because they were changed by the method. del prev_overwrite_dict['send_messages'] -- cgit v1.2.3 From 8c2945f9fee9f6041edaea5b5c98209478b33259 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 18:46:27 -0700 Subject: Silence tests: create channel and overwrite in setUp for silence tests Reduce code redundancy by only defining them once. --- tests/bot/cogs/moderation/test_silence.py | 46 ++++++++++++------------------- 1 file changed, 17 insertions(+), 29 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index fe6045c87..eba8385bc 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -142,6 +142,10 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): asyncio.run(self.cog._init_cog()) # 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 + async def test_sent_correct_message(self): """Appropriate failure/success message was sent by the command.""" test_cases = ( @@ -175,27 +179,19 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_silenced_channel(self): """Channel had `send_message` and `add_reactions` permissions revoked for verified role.""" - channel = MockTextChannel() - overwrite = PermissionOverwrite(send_messages=True, add_reactions=None) - channel.overwrites_for.return_value = overwrite - - self.assertTrue(await self.cog._silence(channel, False, None)) - self.assertFalse(overwrite.send_messages) - self.assertFalse(overwrite.add_reactions) - channel.set_permissions.assert_awaited_once_with( + self.assertTrue(await self.cog._silence(self.channel, False, None)) + self.assertFalse(self.overwrite.send_messages) + self.assertFalse(self.overwrite.add_reactions) + self.channel.set_permissions.assert_awaited_once_with( self.cog._verified_role, - overwrite=overwrite + overwrite=self.overwrite ) async def test_preserved_other_overwrites(self): """Channel's other unrelated overwrites were not changed.""" - channel = MockTextChannel() - overwrite = PermissionOverwrite(stream=True, attach_files=False) - channel.overwrites_for.return_value = overwrite - - prev_overwrite_dict = dict(overwrite) - await self.cog._silence(channel, False, None) - new_overwrite_dict = dict(overwrite) + prev_overwrite_dict = dict(self.overwrite) + await self.cog._silence(self.channel, False, None) + new_overwrite_dict = dict(self.overwrite) # Remove 'send_messages' & 'add_reactions' keys because they were changed by the method. del prev_overwrite_dict['send_messages'] @@ -207,29 +203,21 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_added_removed_notifier(self): """Channel was added to notifier if `persistent` was `True`, and removed if `False`.""" - channel = MockTextChannel() - overwrite = PermissionOverwrite(send_messages=True, add_reactions=None) - channel.overwrites_for.return_value = overwrite - with mock.patch.object(self.cog, "notifier", create=True): with self.subTest(persistent=True): - await self.cog._silence(channel, True, None) + await self.cog._silence(self.channel, True, None) self.cog.notifier.add_channel.assert_called_once() with mock.patch.object(self.cog, "notifier", create=True): with self.subTest(persistent=False): - await self.cog._silence(channel, False, None) + await self.cog._silence(self.channel, False, None) self.cog.notifier.add_channel.assert_not_called() async def test_cached_previous_overwrites(self): """Channel's previous overwrites were cached.""" - channel = MockTextChannel() - overwrite = PermissionOverwrite(send_messages=True, add_reactions=None) - overwrite_json = '{"send_messages": true, "add_reactions": null}' - channel.overwrites_for.return_value = overwrite - - await self.cog._silence(channel, False, None) - self.cog.muted_channel_perms.set.assert_called_once_with(channel.id, overwrite_json) + overwrite_json = '{"send_messages": true, "add_reactions": false}' + await self.cog._silence(self.channel, False, None) + self.cog.muted_channel_perms.set.assert_called_once_with(self.channel.id, overwrite_json) @autospec(Silence, "muted_channel_times", pass_mocks=False) -- cgit v1.2.3 From 282596d1414613e05ee8b956393913da976b35e3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 19:13:18 -0700 Subject: Silence tests: fix mock for _init_task An `AsyncMock` fails because it returns a coroutine which may only be awaited once. However, an `asyncio.Future` is perfect because it is easy to create and can be awaited repeatedly, just like the actual `asyncio.Task` that is being mocked. --- tests/bot/cogs/moderation/test_silence.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index eba8385bc..5d42d8c36 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -138,7 +138,8 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.bot = MockBot() self.cog = Silence(self.bot) - self.cog._init_task = mock.AsyncMock()() + self.cog._init_task = asyncio.Future() + self.cog._init_task.set_result(None) asyncio.run(self.cog._init_cog()) # Populate instance attributes. @@ -229,7 +230,8 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.bot = MockBot() self.cog = Silence(self.bot) - self.cog._init_task = mock.AsyncMock()() + self.cog._init_task = asyncio.Future() + self.cog._init_task.set_result(None) perms_cache = mock.create_autospec(self.cog.muted_channel_perms, spec_set=True) self.cog.muted_channel_perms = perms_cache -- cgit v1.2.3 From eda342b40ccd941050a1421ef1907cb2790c1cde Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 19:15:53 -0700 Subject: Silence tests: add a test for the time cache --- tests/bot/cogs/moderation/test_silence.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 5d42d8c36..1ae17177f 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -1,5 +1,6 @@ import asyncio import unittest +from datetime import datetime, timezone from unittest import mock from unittest.mock import Mock @@ -220,6 +221,20 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._silence(self.channel, False, None) self.cog.muted_channel_perms.set.assert_called_once_with(self.channel.id, overwrite_json) + @autospec("bot.cogs.moderation.silence", "datetime") + async def test_cached_unsilence_time(self, datetime_mock): + """The UTC POSIX timestamp for the unsilence was cached.""" + now_timestamp = 100 + duration = 15 + timestamp = now_timestamp + duration * 60 + datetime_mock.now.return_value = datetime.fromtimestamp(now_timestamp, tz=timezone.utc) + + ctx = MockContext(channel=self.channel) + await self.cog.silence.callback(self.cog, ctx, duration) + + self.cog.muted_channel_times.set.assert_awaited_once_with(ctx.channel.id, timestamp) + datetime_mock.now.assert_called_once_with(tz=timezone.utc) # Ensure it's using an aware dt. + @autospec(Silence, "muted_channel_times", pass_mocks=False) class UnsilenceTests(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From 5eec1c2db319ccdb1f71c1a25fa541eeb7a2707a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 19:17:51 -0700 Subject: Silence tests: add a test for caching permanent times --- tests/bot/cogs/moderation/test_silence.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 1ae17177f..2e756a88f 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -235,6 +235,12 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.cog.muted_channel_times.set.assert_awaited_once_with(ctx.channel.id, timestamp) datetime_mock.now.assert_called_once_with(tz=timezone.utc) # Ensure it's using an aware dt. + async def test_cached_indefinite_time(self): + """A value of -1 was cached for a permanent silence.""" + ctx = MockContext(channel=self.channel) + await self.cog.silence.callback(self.cog, ctx, None) + self.cog.muted_channel_times.set.assert_awaited_once_with(ctx.channel.id, -1) + @autospec(Silence, "muted_channel_times", pass_mocks=False) class UnsilenceTests(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From 1e1d358ae38bb9d554e993fb61ee8f0b52f977b5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 19:25:15 -0700 Subject: Silence tests: add tests for scheduling tasks --- tests/bot/cogs/moderation/test_silence.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 2e756a88f..979b4f4e5 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -241,6 +241,18 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog.silence.callback(self.cog, ctx, None) self.cog.muted_channel_times.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) + await self.cog.silence.callback(self.cog, ctx) + self.cog.scheduler.schedule_later.assert_called_once() + + async def test_permanent_not_scheduled(self): + """A task was not scheduled for a permanent silence.""" + ctx = MockContext(channel=self.channel) + await self.cog.silence.callback(self.cog, ctx, None) + self.cog.scheduler.schedule_later.assert_not_called() + @autospec(Silence, "muted_channel_times", pass_mocks=False) class UnsilenceTests(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From d4fbd675d9803cc664909c19fcec8a430524f918 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 19:26:48 -0700 Subject: Silence tests: add a test for deletion from the time cache --- tests/bot/cogs/moderation/test_silence.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 979b4f4e5..6f913b8f9 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -319,6 +319,11 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._unsilence(self.channel) self.cog.muted_channel_perms.delete.assert_awaited_once_with(self.channel.id) + async def test_deleted_cached_time(self): + """Channel was deleted from the timestamp cache.""" + await self.cog._unsilence(self.channel) + self.cog.muted_channel_times.delete.assert_awaited_once_with(self.channel.id) + async def test_preserved_other_overwrites(self): """Channel's other unrelated overwrites were not changed.""" prev_overwrite_dict = dict(self.overwrite) -- cgit v1.2.3 From 67f88e0b63ec9ac198b8204d4b07e6f7ee67937b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 19:28:15 -0700 Subject: Silence tests: add a test for task cancellation --- tests/bot/cogs/moderation/test_silence.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 6f913b8f9..9e81df9c4 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -324,6 +324,11 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._unsilence(self.channel) self.cog.muted_channel_times.delete.assert_awaited_once_with(self.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) + async def test_preserved_other_overwrites(self): """Channel's other unrelated overwrites were not changed.""" prev_overwrite_dict = dict(self.overwrite) -- cgit v1.2.3 From cc956f24e1f748dbe97fc6bd96383d22a494c5ed Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 19:50:14 -0700 Subject: Silence tests: add a test for default overwrites on cache miss Use a False for `add_reactions` in the mock overwrite rather than None to be sure the default (also None) is actually set for it. Fix channels set by `_init_cog` not being mocked properly. --- tests/bot/cogs/moderation/test_silence.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 9e81df9c4..992906a50 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -261,7 +261,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): @autospec(Silence, "_reschedule", pass_mocks=False) @autospec("bot.cogs.moderation.silence", "Scheduler", "SilenceNotifier", pass_mocks=False) def setUp(self) -> None: - self.bot = MockBot() + self.bot = MockBot(get_channel=lambda _: MockTextChannel()) self.cog = Silence(self.bot) self.cog._init_task = asyncio.Future() self.cog._init_task.set_result(None) @@ -271,7 +271,8 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): asyncio.run(self.cog._init_cog()) # Populate instance attributes. - perms_cache.get.return_value = '{"send_messages": true, "add_reactions": null}' + self.cog.scheduler.__contains__.return_value = True + perms_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 @@ -298,15 +299,29 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.assertFalse(await self.cog._unsilence(channel)) channel.set_permissions.assert_not_called() - async def test_unsilenced_channel(self): + 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( - self.cog._verified_role, overwrite=self.overwrite + self.cog._verified_role, + overwrite=self.overwrite, ) # Recall that these values are determined by the fixture. self.assertTrue(self.overwrite.send_messages) + self.assertFalse(self.overwrite.add_reactions) + + async def test_cache_miss_used_default_overwrites(self): + """Both overwrites were set to None due previous values not being found in the cache.""" + self.cog.muted_channel_perms.get.return_value = None + + await self.cog._unsilence(self.channel) + self.channel.set_permissions.assert_awaited_once_with( + self.cog._verified_role, + overwrite=self.overwrite, + ) + + self.assertIsNone(self.overwrite.send_messages) self.assertIsNone(self.overwrite.add_reactions) async def test_removed_notifier(self): -- cgit v1.2.3 From e4548b2505cf4765cfd2a2c1a1762212cc5cba25 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 19:54:04 -0700 Subject: Silence tests: add a test for a mod alert on cache miss --- tests/bot/cogs/moderation/test_silence.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 992906a50..ccc908ee4 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -324,6 +324,13 @@ 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.""" + self.cog.muted_channel_perms.get.return_value = None + + await self.cog._unsilence(self.channel) + self.cog._mod_alerts_channel.send.assert_awaited_once() + async def test_removed_notifier(self): """Channel was removed from `notifier`.""" await self.cog._unsilence(self.channel) -- cgit v1.2.3 From 33fb55cbe431211f99acfbce22129c48a60a1e6b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 19:58:26 -0700 Subject: Silence tests: also test that cache misses preserve other overwrites --- tests/bot/cogs/moderation/test_silence.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index ccc908ee4..71608d3f9 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -352,15 +352,19 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.cog.scheduler.cancel.assert_called_once_with(self.channel.id) async def test_preserved_other_overwrites(self): - """Channel's other unrelated overwrites were not changed.""" - prev_overwrite_dict = dict(self.overwrite) - await self.cog._unsilence(self.channel) - new_overwrite_dict = dict(self.overwrite) - - # Remove 'send_messages' & 'add_reactions' keys because they were changed by the method. - del prev_overwrite_dict['send_messages'] - del prev_overwrite_dict['add_reactions'] - del new_overwrite_dict['send_messages'] - del new_overwrite_dict['add_reactions'] - - self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) + """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.muted_channel_perms.get.return_value = overwrite_json + + prev_overwrite_dict = dict(self.overwrite) + await self.cog._unsilence(self.channel) + new_overwrite_dict = dict(self.overwrite) + + # Remove these keys because they were modified by the unsilence. + del prev_overwrite_dict['send_messages'] + del prev_overwrite_dict['add_reactions'] + del new_overwrite_dict['send_messages'] + del new_overwrite_dict['add_reactions'] + + self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) -- cgit v1.2.3 From 83d74c56af2a777a2a4f6f7f5347598d0000a66b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 20:14:46 -0700 Subject: Silence tests: assert against message constants Duplicating strings in assertions is redundant, closely coupled, and less maintainable. --- bot/cogs/moderation/silence.py | 26 +++++++++++++++++--------- tests/bot/cogs/moderation/test_silence.py | 13 +++++++------ 2 files changed, 24 insertions(+), 15 deletions(-) (limited to 'tests') diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py index de799f64f..9732248ff 100644 --- a/bot/cogs/moderation/silence.py +++ b/bot/cogs/moderation/silence.py @@ -17,6 +17,17 @@ from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) +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 = Emojis.check_mark + " silenced current channel for {duration} minute(s)." + +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 current " + f"overwrites were set manually. Please edit them manually to unsilence." +) +MSG_UNSILENCE_SUCCESS = f"{Emojis.check_mark} unsilenced current channel." + class SilenceNotifier(tasks.Loop): """Loop notifier for posting notices to `alert_channel` containing added channels.""" @@ -96,15 +107,15 @@ class Silence(commands.Cog): log.debug(f"{ctx.author} is silencing channel #{ctx.channel}.") if not await self._silence(ctx.channel, persistent=(duration is None), duration=duration): - await ctx.send(f"{Emojis.cross_mark} current channel is already silenced.") + await ctx.send(MSG_SILENCE_FAIL) return if duration is None: - await ctx.send(f"{Emojis.check_mark} silenced current channel indefinitely.") + await ctx.send(MSG_SILENCE_PERMANENT) await self.muted_channel_times.set(ctx.channel.id, -1) return - await ctx.send(f"{Emojis.check_mark} silenced current channel for {duration} minute(s).") + await ctx.send(MSG_SILENCE_SUCCESS.format(duration=duration)) self.scheduler.schedule_later(duration * 60, ctx.channel.id, ctx.invoke(self.unsilence)) unsilence_time = (datetime.now(tz=timezone.utc) + timedelta(minutes=duration)) @@ -126,14 +137,11 @@ class Silence(commands.Cog): if not await self._unsilence(channel): overwrite = channel.overwrites_for(self._verified_role) if overwrite.send_messages is False and overwrite.add_reactions is False: - await channel.send( - f"{Emojis.cross_mark} current channel was not unsilenced because the current " - f"overwrites were set manually. Please edit them manually to unsilence." - ) + await channel.send(MSG_UNSILENCE_MANUAL) else: - await channel.send(f"{Emojis.cross_mark} current channel was not silenced.") + await channel.send(MSG_UNSILENCE_FAIL) else: - await channel.send(f"{Emojis.check_mark} unsilenced current channel.") + await channel.send(MSG_UNSILENCE_SUCCESS) async def _silence(self, channel: TextChannel, persistent: bool, duration: Optional[int]) -> bool: """ diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 71608d3f9..168794b6f 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -6,8 +6,9 @@ from unittest.mock import Mock from discord import PermissionOverwrite +from bot.cogs.moderation import silence from bot.cogs.moderation.silence import Silence, SilenceNotifier -from bot.constants import Channels, Emojis, Guild, Roles +from bot.constants import Channels, Guild, Roles from tests.helpers import MockBot, MockContext, MockTextChannel, autospec @@ -151,9 +152,9 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_sent_correct_message(self): """Appropriate failure/success message was sent by the command.""" test_cases = ( - (0.0001, f"{Emojis.check_mark} silenced current channel for 0.0001 minute(s).", True,), - (None, f"{Emojis.check_mark} silenced current channel indefinitely.", True,), - (5, f"{Emojis.cross_mark} current channel is already silenced.", False,), + (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() @@ -280,8 +281,8 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): async def test_sent_correct_message(self): """Appropriate failure/success message was sent by the command.""" test_cases = ( - (True, f"{Emojis.check_mark} unsilenced current channel."), - (False, f"{Emojis.cross_mark} current channel was not silenced.") + (True, silence.MSG_UNSILENCE_SUCCESS), + (False, silence.MSG_UNSILENCE_FAIL) ) for was_unsilenced, message in test_cases: ctx = MockContext() -- cgit v1.2.3 From ed30502710e805de5e3793b762a3848a0295582d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 20:18:14 -0700 Subject: Silence tests: add a subtest for the manual unsilence message --- tests/bot/cogs/moderation/test_silence.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 168794b6f..254480a6d 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -280,14 +280,17 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): async def test_sent_correct_message(self): """Appropriate failure/success message was sent by the command.""" + unsilenced_overwrite = PermissionOverwrite(send_messages=True, add_reactions=True) test_cases = ( - (True, silence.MSG_UNSILENCE_SUCCESS), - (False, silence.MSG_UNSILENCE_FAIL) + (True, silence.MSG_UNSILENCE_SUCCESS, unsilenced_overwrite), + (False, silence.MSG_UNSILENCE_FAIL, unsilenced_overwrite), + (False, silence.MSG_UNSILENCE_MANUAL, self.overwrite), ) - for was_unsilenced, message in test_cases: + for was_unsilenced, message, overwrite in test_cases: ctx = MockContext() - with self.subTest(was_unsilenced=was_unsilenced, message=message): + 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) -- cgit v1.2.3 From f9d4081efc41c7ab9f5e6362a0ab4fab5bc88cd8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 20:23:15 -0700 Subject: Silence tests: access everything via the silence module The module is imported anyway to keep imports short and clean. Using it in patch targets is shorter and allows for the two imports from the module to be removed. --- tests/bot/cogs/moderation/test_silence.py | 47 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 24 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 254480a6d..0a93cc623 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -7,7 +7,6 @@ from unittest.mock import Mock from discord import PermissionOverwrite from bot.cogs.moderation import silence -from bot.cogs.moderation.silence import Silence, SilenceNotifier from bot.constants import Channels, Guild, Roles from tests.helpers import MockBot, MockContext, MockTextChannel, autospec @@ -15,7 +14,7 @@ from tests.helpers import MockBot, MockContext, MockTextChannel, autospec class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.alert_channel = MockTextChannel() - self.notifier = SilenceNotifier(self.alert_channel) + self.notifier = silence.SilenceNotifier(self.alert_channel) self.notifier.stop = self.notifier_stop_mock = Mock() self.notifier.start = self.notifier_start_mock = Mock() @@ -75,41 +74,41 @@ class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): self.alert_channel.send.assert_not_called() -@autospec(Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) +@autospec(silence.Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) class SilenceCogTests(unittest.IsolatedAsyncioTestCase): """Tests for the general functionality of the Silence cog.""" - @autospec("bot.cogs.moderation.silence", "Scheduler", pass_mocks=False) + @autospec(silence, "Scheduler", pass_mocks=False) def setUp(self) -> None: self.bot = MockBot() - self.cog = Silence(self.bot) + self.cog = silence.Silence(self.bot) - @autospec(Silence, "_reschedule", pass_mocks=False) - @autospec("bot.cogs.moderation.silence", "SilenceNotifier", pass_mocks=False) + @autospec(silence.Silence, "_reschedule", pass_mocks=False) + @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_init_cog_got_guild(self): """Bot got guild after it became available.""" await self.cog._init_cog() self.bot.wait_until_guild_available.assert_awaited_once() self.bot.get_guild.assert_called_once_with(Guild.id) - @autospec(Silence, "_reschedule", pass_mocks=False) - @autospec("bot.cogs.moderation.silence", "SilenceNotifier", pass_mocks=False) + @autospec(silence.Silence, "_reschedule", pass_mocks=False) + @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_init_cog_got_role(self): """Got `Roles.verified` role from guild.""" await self.cog._init_cog() guild = self.bot.get_guild() guild.get_role.assert_called_once_with(Roles.verified) - @autospec(Silence, "_reschedule", pass_mocks=False) - @autospec("bot.cogs.moderation.silence", "SilenceNotifier", pass_mocks=False) + @autospec(silence.Silence, "_reschedule", pass_mocks=False) + @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_init_cog_got_channels(self): """Got channels from bot.""" await self.cog._init_cog() self.bot.get_channel.called_once_with(Channels.mod_alerts) self.bot.get_channel.called_once_with(Channels.mod_log) - @autospec(Silence, "_reschedule", pass_mocks=False) - @autospec("bot.cogs.moderation.silence", "SilenceNotifier") + @autospec(silence.Silence, "_reschedule", pass_mocks=False) + @autospec(silence, "SilenceNotifier") async def test_init_cog_got_notifier(self, notifier): """Notifier was started with channel.""" mod_log = MockTextChannel() @@ -122,8 +121,8 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): self.cog.cog_unload() self.cog.scheduler.cancel_all.assert_called_once_with() - @autospec("bot.cogs.moderation.silence", "with_role_check") - @mock.patch("bot.cogs.moderation.silence.MODERATION_ROLES", new=(1, 2, 3)) + @autospec(silence, "with_role_check") + @mock.patch.object(silence, "MODERATION_ROLES", new=(1, 2, 3)) def test_cog_check(self, role_check): """Role check was called with `MODERATION_ROLES`""" ctx = MockContext() @@ -131,15 +130,15 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): role_check.assert_called_once_with(ctx, *(1, 2, 3)) -@autospec(Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) +@autospec(silence.Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) class SilenceTests(unittest.IsolatedAsyncioTestCase): """Tests for the silence command and its related helper methods.""" - @autospec(Silence, "_reschedule", pass_mocks=False) - @autospec("bot.cogs.moderation.silence", "Scheduler", "SilenceNotifier", pass_mocks=False) + @autospec(silence.Silence, "_reschedule", pass_mocks=False) + @autospec(silence, "Scheduler", "SilenceNotifier", pass_mocks=False) def setUp(self) -> None: self.bot = MockBot() - self.cog = Silence(self.bot) + self.cog = silence.Silence(self.bot) self.cog._init_task = asyncio.Future() self.cog._init_task.set_result(None) @@ -222,7 +221,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): await self.cog._silence(self.channel, False, None) self.cog.muted_channel_perms.set.assert_called_once_with(self.channel.id, overwrite_json) - @autospec("bot.cogs.moderation.silence", "datetime") + @autospec(silence, "datetime") async def test_cached_unsilence_time(self, datetime_mock): """The UTC POSIX timestamp for the unsilence was cached.""" now_timestamp = 100 @@ -255,15 +254,15 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.cog.scheduler.schedule_later.assert_not_called() -@autospec(Silence, "muted_channel_times", pass_mocks=False) +@autospec(silence.Silence, "muted_channel_times", pass_mocks=False) class UnsilenceTests(unittest.IsolatedAsyncioTestCase): """Tests for the unsilence command and its related helper methods.""" - @autospec(Silence, "_reschedule", pass_mocks=False) - @autospec("bot.cogs.moderation.silence", "Scheduler", "SilenceNotifier", pass_mocks=False) + @autospec(silence.Silence, "_reschedule", pass_mocks=False) + @autospec(silence, "Scheduler", "SilenceNotifier", pass_mocks=False) def setUp(self) -> None: self.bot = MockBot(get_channel=lambda _: MockTextChannel()) - self.cog = Silence(self.bot) + self.cog = silence.Silence(self.bot) self.cog._init_task = asyncio.Future() self.cog._init_task.set_result(None) -- cgit v1.2.3 From cbdc14a3abbcbee644e9d4a6f3ffde125a7c91f1 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 20:36:48 -0700 Subject: Silence tests: remove _reschedule patch for cog tests They don't do anything because they patch the class rather than the instance. It's too late for patching the instance to work since the `setUp` fixture, which instantiates the cog, executes before the patches do. Patching `setUp` would work (and its done in the other test cases), but some tests in this case will need the unpatched function too. Patching it doesn't serve much benefit to most tests anyway, so it's not worth the effort trying to make them work where they aren't needed. --- tests/bot/cogs/moderation/test_silence.py | 4 ---- 1 file changed, 4 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 0a93cc623..667d61776 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -83,7 +83,6 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): self.bot = MockBot() self.cog = silence.Silence(self.bot) - @autospec(silence.Silence, "_reschedule", pass_mocks=False) @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_init_cog_got_guild(self): """Bot got guild after it became available.""" @@ -91,7 +90,6 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): self.bot.wait_until_guild_available.assert_awaited_once() self.bot.get_guild.assert_called_once_with(Guild.id) - @autospec(silence.Silence, "_reschedule", pass_mocks=False) @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_init_cog_got_role(self): """Got `Roles.verified` role from guild.""" @@ -99,7 +97,6 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): guild = self.bot.get_guild() guild.get_role.assert_called_once_with(Roles.verified) - @autospec(silence.Silence, "_reschedule", pass_mocks=False) @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_init_cog_got_channels(self): """Got channels from bot.""" @@ -107,7 +104,6 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): self.bot.get_channel.called_once_with(Channels.mod_alerts) self.bot.get_channel.called_once_with(Channels.mod_log) - @autospec(silence.Silence, "_reschedule", pass_mocks=False) @autospec(silence, "SilenceNotifier") async def test_init_cog_got_notifier(self, notifier): """Notifier was started with channel.""" -- cgit v1.2.3 From 366f975bbb22c45b9e071644ab4053416bf351fb Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 17 Aug 2020 20:37:24 -0700 Subject: Silence tests: add a test for _init_cog rescheduling unsilences --- tests/bot/cogs/moderation/test_silence.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 667d61776..5deed2d0b 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -112,6 +112,13 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): await self.cog._init_cog() notifier.assert_called_once_with(self.cog._mod_log_channel) + @autospec(silence, "SilenceNotifier", pass_mocks=False) + async def test_init_cog_rescheduled(self): + """`_reschedule_` coroutine was awaited.""" + self.cog._reschedule = mock.create_autospec(self.cog._reschedule, spec_set=True) + await self.cog._init_cog() + self.cog._reschedule.assert_awaited_once_with() + def test_cog_unload_cancelled_tasks(self): """All scheduled tasks were cancelled.""" self.cog.cog_unload() -- cgit v1.2.3 From d5032459bfe1bbbc10ffc3b95809e0fc377de60c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 18 Aug 2020 10:19:17 -0700 Subject: Silence tests: test the scheduler skips missing channels --- tests/bot/cogs/moderation/test_silence.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 5deed2d0b..2c8059752 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -133,6 +133,31 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): role_check.assert_called_once_with(ctx, *(1, 2, 3)) +@autospec(silence.Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) +class RescheduleTests(unittest.IsolatedAsyncioTestCase): + """Tests for the rescheduling of cached unsilences.""" + + @autospec(silence, "Scheduler", "SilenceNotifier", pass_mocks=False) + def setUp(self): + self.bot = MockBot() + self.cog = silence.Silence(self.bot) + self.cog._unsilence_wrapper = mock.create_autospec(self.cog._unsilence_wrapper, spec_set=True) + + with mock.patch.object(self.cog, "_reschedule", spec_set=True, autospec=True): + asyncio.run(self.cog._init_cog()) # Populate instance attributes. + + async def test_skipped_missing_channel(self): + """Did nothing because the channel couldn't be retrieved.""" + self.cog.muted_channel_times.items.return_value = [(123, -1), (123, 1), (123, 100000000000)] + self.bot.get_channel.return_value = None + + await self.cog._reschedule() + + self.cog.notifier.add_channel.assert_not_called() + self.cog._unsilence_wrapper.assert_not_called() + self.cog.scheduler.schedule_later.assert_not_called() + + @autospec(silence.Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) class SilenceTests(unittest.IsolatedAsyncioTestCase): """Tests for the silence command and its related helper methods.""" -- cgit v1.2.3 From d44e3f795f8c56a1fbbce2833b27474b263e911b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 18 Aug 2020 10:53:33 -0700 Subject: Silence tests: test the rescheduler adds permanent silence to notifier --- tests/bot/cogs/moderation/test_silence.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 2c8059752..6e8c9ff38 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -157,6 +157,20 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): self.cog._unsilence_wrapper.assert_not_called() self.cog.scheduler.schedule_later.assert_not_called() + async def test_added_permanent_to_notifier(self): + """Permanently silenced channels were added to the notifier.""" + channels = [MockTextChannel(id=123), MockTextChannel(id=456)] + self.bot.get_channel.side_effect = channels + self.cog.muted_channel_times.items.return_value = [(123, -1), (456, -1)] + + await self.cog._reschedule() + + self.cog.notifier.add_channel.assert_any_call(channels[0]) + self.cog.notifier.add_channel.assert_any_call(channels[1]) + + self.cog._unsilence_wrapper.assert_not_called() + self.cog.scheduler.schedule_later.assert_not_called() + @autospec(silence.Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) class SilenceTests(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From 5f23f6630cd1c44d129d23e4becd9fce7f76135d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 18 Aug 2020 10:57:17 -0700 Subject: Silence tests: test the rescheduler unsilences expired silences --- tests/bot/cogs/moderation/test_silence.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 6e8c9ff38..d9ff13595 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -171,6 +171,20 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): self.cog._unsilence_wrapper.assert_not_called() self.cog.scheduler.schedule_later.assert_not_called() + async def test_unsilenced_expired(self): + """Unsilenced expired silences.""" + channels = [MockTextChannel(id=123), MockTextChannel(id=456)] + self.bot.get_channel.side_effect = channels + self.cog.muted_channel_times.items.return_value = [(123, 100), (456, 200)] + + await self.cog._reschedule() + + self.cog._unsilence_wrapper.assert_any_call(channels[0]) + self.cog._unsilence_wrapper.assert_any_call(channels[1]) + + self.cog.notifier.add_channel.assert_not_called() + self.cog.scheduler.schedule_later.assert_not_called() + @autospec(silence.Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) class SilenceTests(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From 7fadf2d531562dcc7e78bcb70d59d0a0575a18be Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 18 Aug 2020 11:56:49 -0700 Subject: Silence tests: add a test for rescheduling active silences --- tests/bot/cogs/moderation/test_silence.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index d9ff13595..3d111341b 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -11,6 +11,13 @@ from bot.constants import Channels, Guild, Roles from tests.helpers import MockBot, MockContext, MockTextChannel, autospec +# Have to subclass it because builtins can't be patched. +class PatchedDatetime(datetime): + """A datetime object with a mocked now() function.""" + + now = mock.create_autospec(datetime, "now") + + class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.alert_channel = MockTextChannel() @@ -185,6 +192,28 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): self.cog.notifier.add_channel.assert_not_called() self.cog.scheduler.schedule_later.assert_not_called() + @mock.patch.object(silence, "datetime", new=PatchedDatetime) + async def test_rescheduled_active(self): + """Rescheduled active silences.""" + channels = [MockTextChannel(id=123), MockTextChannel(id=456)] + self.bot.get_channel.side_effect = channels + self.cog.muted_channel_times.items.return_value = [(123, 2000), (456, 3000)] + silence.datetime.now.return_value = datetime.fromtimestamp(1000, tz=timezone.utc) + + self.cog._unsilence_wrapper = mock.MagicMock() + unsilence_return = self.cog._unsilence_wrapper.return_value + + await self.cog._reschedule() + + # Yuck. + calls = [mock.call(1000, 123, unsilence_return), mock.call(2000, 456, unsilence_return)] + self.cog.scheduler.schedule_later.assert_has_calls(calls) + + unsilence_calls = [mock.call(channel) for channel in channels] + self.cog._unsilence_wrapper.assert_has_calls(unsilence_calls) + + self.cog.notifier.add_channel.assert_not_called() + @autospec(silence.Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) class SilenceTests(unittest.IsolatedAsyncioTestCase): -- cgit v1.2.3 From a9ddbf346d95e67731196d8d822835330b6992af Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 18 Aug 2020 12:02:29 -0700 Subject: Silence tests: more accurately assert the silence cmd schedule a task --- tests/bot/cogs/moderation/test_silence.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 3d111341b..bc41422ef 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -328,9 +328,13 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_scheduled_task(self): """An unsilence task was scheduled.""" - ctx = MockContext(channel=self.channel) - await self.cog.silence.callback(self.cog, ctx) - self.cog.scheduler.schedule_later.assert_called_once() + ctx = MockContext(channel=self.channel, invoke=mock.MagicMock()) + + await self.cog.silence.callback(self.cog, ctx, 5) + + 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) async def test_permanent_not_scheduled(self): """A task was not scheduled for a permanent silence.""" -- cgit v1.2.3 From 40c6f688eb0e317b1489b069f263f25b202a345c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 18 Aug 2020 12:15:36 -0700 Subject: Silence tests: remove unnecessary spec_set args It's not really necessary to set to True when mocking functions. --- tests/bot/cogs/moderation/test_silence.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index bc41422ef..5c6d677ca 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -122,7 +122,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_init_cog_rescheduled(self): """`_reschedule_` coroutine was awaited.""" - self.cog._reschedule = mock.create_autospec(self.cog._reschedule, spec_set=True) + self.cog._reschedule = mock.create_autospec(self.cog._reschedule) await self.cog._init_cog() self.cog._reschedule.assert_awaited_once_with() @@ -148,9 +148,9 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): def setUp(self): self.bot = MockBot() self.cog = silence.Silence(self.bot) - self.cog._unsilence_wrapper = mock.create_autospec(self.cog._unsilence_wrapper, spec_set=True) + self.cog._unsilence_wrapper = mock.create_autospec(self.cog._unsilence_wrapper) - with mock.patch.object(self.cog, "_reschedule", spec_set=True, autospec=True): + with mock.patch.object(self.cog, "_reschedule", autospec=True): asyncio.run(self.cog._init_cog()) # Populate instance attributes. async def test_skipped_missing_channel(self): -- cgit v1.2.3 From 27a00bf29193b1768c298c1455936bb7dc92aaf1 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 18 Aug 2020 12:18:14 -0700 Subject: Silence: rename caches --- bot/cogs/moderation/silence.py | 18 +++++++------- tests/bot/cogs/moderation/test_silence.py | 40 +++++++++++++++---------------- 2 files changed, 29 insertions(+), 29 deletions(-) (limited to 'tests') diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py index 9732248ff..5851be00a 100644 --- a/bot/cogs/moderation/silence.py +++ b/bot/cogs/moderation/silence.py @@ -72,11 +72,11 @@ class Silence(commands.Cog): # Maps muted channel IDs to their previous overwrites for send_message and add_reactions. # Overwrites are stored as JSON. - muted_channel_perms = RedisCache() + previous_overwrites = RedisCache() # Maps muted channel IDs to POSIX timestamps of when they'll be unsilenced. # A timestamp equal to -1 means it's indefinite. - muted_channel_times = RedisCache() + unsilence_timestamps = RedisCache() def __init__(self, bot: Bot): self.bot = bot @@ -112,14 +112,14 @@ class Silence(commands.Cog): if duration is None: await ctx.send(MSG_SILENCE_PERMANENT) - await self.muted_channel_times.set(ctx.channel.id, -1) + await self.unsilence_timestamps.set(ctx.channel.id, -1) return await ctx.send(MSG_SILENCE_SUCCESS.format(duration=duration)) self.scheduler.schedule_later(duration * 60, ctx.channel.id, ctx.invoke(self.unsilence)) unsilence_time = (datetime.now(tz=timezone.utc) + timedelta(minutes=duration)) - await self.muted_channel_times.set(ctx.channel.id, unsilence_time.timestamp()) + await self.unsilence_timestamps.set(ctx.channel.id, unsilence_time.timestamp()) @commands.command(aliases=("unhush",)) async def unsilence(self, ctx: Context) -> None: @@ -160,7 +160,7 @@ class Silence(commands.Cog): overwrite.update(send_messages=False, add_reactions=False) await channel.set_permissions(self._verified_role, overwrite=overwrite) - await self.muted_channel_perms.set(channel.id, json.dumps(prev_overwrites)) + await self.previous_overwrites.set(channel.id, json.dumps(prev_overwrites)) if persistent: log.info(f"Silenced #{channel} ({channel.id}) indefinitely.") @@ -180,7 +180,7 @@ class Silence(commands.Cog): Return `True` if channel permissions were changed, `False` otherwise. """ - prev_overwrites = await self.muted_channel_perms.get(channel.id) + prev_overwrites = await self.previous_overwrites.get(channel.id) if channel.id not in self.scheduler and prev_overwrites is None: log.info(f"Tried to unsilence channel #{channel} ({channel.id}) but the channel was not silenced.") return False @@ -197,8 +197,8 @@ class Silence(commands.Cog): self.scheduler.cancel(channel.id) self.notifier.remove_channel(channel) - await self.muted_channel_perms.delete(channel.id) - await self.muted_channel_times.delete(channel.id) + await self.previous_overwrites.delete(channel.id) + await self.unsilence_timestamps.delete(channel.id) if prev_overwrites is None: await self._mod_alerts_channel.send( @@ -211,7 +211,7 @@ class Silence(commands.Cog): async def _reschedule(self) -> None: """Reschedule unsilencing of active silences and add permanent ones to the notifier.""" - for channel_id, timestamp in await self.muted_channel_times.items(): + for channel_id, timestamp in await self.unsilence_timestamps.items(): channel = self.bot.get_channel(channel_id) if channel is None: log.info(f"Can't reschedule silence for {channel_id}: channel not found.") diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 5c6d677ca..a66d27d08 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -81,7 +81,7 @@ class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): self.alert_channel.send.assert_not_called() -@autospec(silence.Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) +@autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) class SilenceCogTests(unittest.IsolatedAsyncioTestCase): """Tests for the general functionality of the Silence cog.""" @@ -140,7 +140,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): role_check.assert_called_once_with(ctx, *(1, 2, 3)) -@autospec(silence.Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) +@autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) class RescheduleTests(unittest.IsolatedAsyncioTestCase): """Tests for the rescheduling of cached unsilences.""" @@ -155,7 +155,7 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): async def test_skipped_missing_channel(self): """Did nothing because the channel couldn't be retrieved.""" - self.cog.muted_channel_times.items.return_value = [(123, -1), (123, 1), (123, 100000000000)] + self.cog.unsilence_timestamps.items.return_value = [(123, -1), (123, 1), (123, 100000000000)] self.bot.get_channel.return_value = None await self.cog._reschedule() @@ -168,7 +168,7 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): """Permanently silenced channels were added to the notifier.""" channels = [MockTextChannel(id=123), MockTextChannel(id=456)] self.bot.get_channel.side_effect = channels - self.cog.muted_channel_times.items.return_value = [(123, -1), (456, -1)] + self.cog.unsilence_timestamps.items.return_value = [(123, -1), (456, -1)] await self.cog._reschedule() @@ -182,7 +182,7 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): """Unsilenced expired silences.""" channels = [MockTextChannel(id=123), MockTextChannel(id=456)] self.bot.get_channel.side_effect = channels - self.cog.muted_channel_times.items.return_value = [(123, 100), (456, 200)] + self.cog.unsilence_timestamps.items.return_value = [(123, 100), (456, 200)] await self.cog._reschedule() @@ -197,7 +197,7 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): """Rescheduled active silences.""" channels = [MockTextChannel(id=123), MockTextChannel(id=456)] self.bot.get_channel.side_effect = channels - self.cog.muted_channel_times.items.return_value = [(123, 2000), (456, 3000)] + self.cog.unsilence_timestamps.items.return_value = [(123, 2000), (456, 3000)] silence.datetime.now.return_value = datetime.fromtimestamp(1000, tz=timezone.utc) self.cog._unsilence_wrapper = mock.MagicMock() @@ -215,7 +215,7 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): self.cog.notifier.add_channel.assert_not_called() -@autospec(silence.Silence, "muted_channel_perms", "muted_channel_times", pass_mocks=False) +@autospec(silence.Silence, "previous_overwrites", "unsilence_timestamps", pass_mocks=False) class SilenceTests(unittest.IsolatedAsyncioTestCase): """Tests for the silence command and its related helper methods.""" @@ -304,7 +304,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): """Channel's previous overwrites were cached.""" overwrite_json = '{"send_messages": true, "add_reactions": false}' await self.cog._silence(self.channel, False, None) - self.cog.muted_channel_perms.set.assert_called_once_with(self.channel.id, overwrite_json) + self.cog.previous_overwrites.set.assert_called_once_with(self.channel.id, overwrite_json) @autospec(silence, "datetime") async def test_cached_unsilence_time(self, datetime_mock): @@ -317,14 +317,14 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): ctx = MockContext(channel=self.channel) await self.cog.silence.callback(self.cog, ctx, duration) - self.cog.muted_channel_times.set.assert_awaited_once_with(ctx.channel.id, timestamp) + self.cog.unsilence_timestamps.set.assert_awaited_once_with(ctx.channel.id, timestamp) datetime_mock.now.assert_called_once_with(tz=timezone.utc) # Ensure it's using an aware dt. async def test_cached_indefinite_time(self): """A value of -1 was cached for a permanent silence.""" ctx = MockContext(channel=self.channel) await self.cog.silence.callback(self.cog, ctx, None) - self.cog.muted_channel_times.set.assert_awaited_once_with(ctx.channel.id, -1) + self.cog.unsilence_timestamps.set.assert_awaited_once_with(ctx.channel.id, -1) async def test_scheduled_task(self): """An unsilence task was scheduled.""" @@ -343,7 +343,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.cog.scheduler.schedule_later.assert_not_called() -@autospec(silence.Silence, "muted_channel_times", pass_mocks=False) +@autospec(silence.Silence, "unsilence_timestamps", pass_mocks=False) class UnsilenceTests(unittest.IsolatedAsyncioTestCase): """Tests for the unsilence command and its related helper methods.""" @@ -355,13 +355,13 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): self.cog._init_task = asyncio.Future() self.cog._init_task.set_result(None) - perms_cache = mock.create_autospec(self.cog.muted_channel_perms, spec_set=True) - self.cog.muted_channel_perms = perms_cache + overwrites_cache = mock.create_autospec(self.cog.previous_overwrites, spec_set=True) + self.cog.previous_overwrites = overwrites_cache asyncio.run(self.cog._init_cog()) # Populate instance attributes. self.cog.scheduler.__contains__.return_value = True - perms_cache.get.return_value = '{"send_messages": true, "add_reactions": false}' + 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 @@ -385,7 +385,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): 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 - self.cog.muted_channel_perms.get.return_value = None + self.cog.previous_overwrites.get.return_value = None channel = MockTextChannel() self.assertFalse(await self.cog._unsilence(channel)) @@ -405,7 +405,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): async def test_cache_miss_used_default_overwrites(self): """Both overwrites were set to None due previous values not being found in the cache.""" - self.cog.muted_channel_perms.get.return_value = None + self.cog.previous_overwrites.get.return_value = None await self.cog._unsilence(self.channel) self.channel.set_permissions.assert_awaited_once_with( @@ -418,7 +418,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): async def test_cache_miss_sent_mod_alert(self): """A message was sent to the mod alerts channel.""" - self.cog.muted_channel_perms.get.return_value = None + self.cog.previous_overwrites.get.return_value = None await self.cog._unsilence(self.channel) self.cog._mod_alerts_channel.send.assert_awaited_once() @@ -431,12 +431,12 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): async def test_deleted_cached_overwrite(self): """Channel was deleted from the overwrites cache.""" await self.cog._unsilence(self.channel) - self.cog.muted_channel_perms.delete.assert_awaited_once_with(self.channel.id) + self.cog.previous_overwrites.delete.assert_awaited_once_with(self.channel.id) async def test_deleted_cached_time(self): """Channel was deleted from the timestamp cache.""" await self.cog._unsilence(self.channel) - self.cog.muted_channel_times.delete.assert_awaited_once_with(self.channel.id) + self.cog.unsilence_timestamps.delete.assert_awaited_once_with(self.channel.id) async def test_cancelled_task(self): """The scheduled unsilence task should be cancelled.""" @@ -447,7 +447,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): """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.muted_channel_perms.get.return_value = overwrite_json + self.cog.previous_overwrites.get.return_value = overwrite_json prev_overwrite_dict = dict(self.overwrite) await self.cog._unsilence(self.channel) -- cgit v1.2.3 From 2fd2c77035e87dde009c39aa7345e4871d5b41df Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 18 Aug 2020 15:02:13 -0700 Subject: Silence: cancel init task when cog unloads --- bot/cogs/moderation/silence.py | 9 +++++++-- tests/bot/cogs/moderation/test_silence.py | 7 +++++-- 2 files changed, 12 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py index 5851be00a..c339fd4d0 100644 --- a/bot/cogs/moderation/silence.py +++ b/bot/cogs/moderation/silence.py @@ -231,8 +231,13 @@ class Silence(commands.Cog): self.scheduler.schedule_later(delta, channel_id, self._unsilence_wrapper(channel)) def cog_unload(self) -> None: - """Cancel scheduled tasks.""" - self.scheduler.cancel_all() + """Cancel the init task and scheduled tasks.""" + # It's important to wait for _init_task (specifically for _reschedule) to be cancelled + # before cancelling scheduled tasks. Otherwise, it's possible for _reschedule to schedule + # more tasks after cancel_all has finished, despite _init_task.cancel being called first. + # This is cause cancel() on its own doesn't block until the task is cancelled. + self._init_task.cancel() + self._init_task.add_done_callback(lambda _: self.scheduler.cancel_all) # This cannot be static (must have a __func__ attribute). def cog_check(self, ctx: Context) -> bool: diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index a66d27d08..d56a731b6 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -127,9 +127,12 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): self.cog._reschedule.assert_awaited_once_with() def test_cog_unload_cancelled_tasks(self): - """All scheduled tasks were cancelled.""" + """The init task was cancelled.""" + self.cog._init_task = asyncio.Future() self.cog.cog_unload() - self.cog.scheduler.cancel_all.assert_called_once_with() + + # It's too annoying to test cancel_all since it's a done callback and wrapped in a lambda. + self.assertTrue(self.cog._init_task.cancelled()) @autospec(silence, "with_role_check") @mock.patch.object(silence, "MODERATION_ROLES", new=(1, 2, 3)) -- cgit v1.2.3 From cf2c03215ef340b9e093828de365563bb6be587a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 6 Oct 2020 13:23:03 -0700 Subject: Silence: refactor _silence * Rename to `_silence_overwrites` * Reduce responsibilities to only setting permission overwrites * Log in `silence` instead * Add to notifier in `silence` instead --- bot/cogs/moderation/silence.py | 27 ++++++++-------------- tests/bot/cogs/moderation/test_silence.py | 38 ++++++++++++++++++------------- 2 files changed, 32 insertions(+), 33 deletions(-) (limited to 'tests') diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py index 08d0328ab..12896022f 100644 --- a/bot/cogs/moderation/silence.py +++ b/bot/cogs/moderation/silence.py @@ -104,17 +104,23 @@ class Silence(commands.Cog): Indefinitely silenced channels get added to a notifier which posts notices every 15 minutes from the start. """ await self._init_task - log.debug(f"{ctx.author} is silencing channel #{ctx.channel}.") - if not await self._silence(ctx.channel, persistent=(duration is None), duration=duration): + channel_info = f"#{ctx.channel} ({ctx.channel.id})" + log.debug(f"{ctx.author} is silencing channel {channel_info}.") + + if not await self._silence_overwrites(ctx.channel): + log.info(f"Tried to silence channel {channel_info} but the channel was already silenced.") await ctx.send(MSG_SILENCE_FAIL) return await self._schedule_unsilence(ctx, duration) if duration is None: + log.info(f"Silenced {channel_info} indefinitely.") await ctx.send(MSG_SILENCE_PERMANENT) else: + self.notifier.add_channel(ctx.channel) + log.info(f"Silenced {channel_info} for {duration} minute(s).") await ctx.send(MSG_SILENCE_SUCCESS.format(duration=duration)) @commands.command(aliases=("unhush",)) @@ -139,31 +145,18 @@ class Silence(commands.Cog): else: await channel.send(MSG_UNSILENCE_SUCCESS) - async def _silence(self, channel: TextChannel, persistent: bool, duration: Optional[int]) -> bool: - """ - Silence `channel` for `self._verified_role`. - - If `persistent` is `True` add `channel` to notifier. - `duration` is only used for logging; if None is passed `persistent` should be True to not log None. - Return `True` if channel permissions were changed, `False` otherwise. - """ + async def _silence_overwrites(self, channel: TextChannel) -> bool: + """Set silence permission overwrites for `channel` and return True if successful.""" overwrite = channel.overwrites_for(self._verified_role) prev_overwrites = dict(send_messages=overwrite.send_messages, add_reactions=overwrite.add_reactions) if channel.id in self.scheduler or all(val is False for val in prev_overwrites.values()): - log.info(f"Tried to silence channel #{channel} ({channel.id}) but the channel was already silenced.") return False overwrite.update(send_messages=False, add_reactions=False) await channel.set_permissions(self._verified_role, overwrite=overwrite) await self.previous_overwrites.set(channel.id, json.dumps(prev_overwrites)) - if persistent: - log.info(f"Silenced #{channel} ({channel.id}) indefinitely.") - self.notifier.add_channel(channel) - return True - - log.info(f"Silenced #{channel} ({channel.id}) for {duration} minute(s).") return True async def _schedule_unsilence(self, ctx: Context, duration: Optional[int]) -> None: diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index d56a731b6..9dbdfd10a 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -245,8 +245,8 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): ) for duration, message, was_silenced in test_cases: ctx = MockContext() - with self.subTest(was_silenced=was_silenced, message=message, duration=duration): - with mock.patch.object(self.cog, "_silence", return_value=was_silenced): + with mock.patch.object(self.cog, "_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) @@ -264,12 +264,12 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): channel = MockTextChannel() channel.overwrites_for.return_value = overwrite - self.assertFalse(await self.cog._silence(channel, True, None)) + self.assertFalse(await self.cog._silence_overwrites(channel)) channel.set_permissions.assert_not_called() async def test_silenced_channel(self): """Channel had `send_message` and `add_reactions` permissions revoked for verified role.""" - self.assertTrue(await self.cog._silence(self.channel, False, None)) + self.assertTrue(await self.cog._silence_overwrites(self.channel)) self.assertFalse(self.overwrite.send_messages) self.assertFalse(self.overwrite.add_reactions) self.channel.set_permissions.assert_awaited_once_with( @@ -280,7 +280,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_preserved_other_overwrites(self): """Channel's other unrelated overwrites were not changed.""" prev_overwrite_dict = dict(self.overwrite) - await self.cog._silence(self.channel, False, None) + await self.cog._silence_overwrites(self.channel) new_overwrite_dict = dict(self.overwrite) # Remove 'send_messages' & 'add_reactions' keys because they were changed by the method. @@ -291,22 +291,28 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) - async def test_added_removed_notifier(self): - """Channel was added to notifier if `persistent` was `True`, and removed if `False`.""" - with mock.patch.object(self.cog, "notifier", create=True): - with self.subTest(persistent=True): - await self.cog._silence(self.channel, True, None) - self.cog.notifier.add_channel.assert_called_once() + async def test_temp_added_to_notifier(self): + """Channel was added to notifier if a duration was set for the silence.""" + with mock.patch.object(self.cog, "_silence_overwrites", return_value=True): + await self.cog.silence.callback(self.cog, MockContext(), 15) + self.cog.notifier.add_channel.assert_called_once() - with mock.patch.object(self.cog, "notifier", create=True): - with self.subTest(persistent=False): - await self.cog._silence(self.channel, False, None) - self.cog.notifier.add_channel.assert_not_called() + async def test_indefinite_not_added_to_notifier(self): + """Channel was not added to notifier if a duration was not set for the silence.""" + with mock.patch.object(self.cog, "_silence_overwrites", return_value=True): + await self.cog.silence.callback(self.cog, MockContext(), None) + self.cog.notifier.add_channel.assert_not_called() + + async def test_silenced_not_added_to_notifier(self): + """Channel was not added to the notifier if it was already silenced.""" + with mock.patch.object(self.cog, "_silence_overwrites", return_value=False): + await self.cog.silence.callback(self.cog, MockContext(), 15) + self.cog.notifier.add_channel.assert_not_called() async def test_cached_previous_overwrites(self): """Channel's previous overwrites were cached.""" overwrite_json = '{"send_messages": true, "add_reactions": false}' - await self.cog._silence(self.channel, False, None) + await self.cog._silence_overwrites(self.channel) self.cog.previous_overwrites.set.assert_called_once_with(self.channel.id, overwrite_json) @autospec(silence, "datetime") -- cgit v1.2.3 From f218c7b0a505416d44b177b0e863575db626d20c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 6 Oct 2020 13:27:42 -0700 Subject: Silence: rename _init_cog to _async_init --- bot/cogs/moderation/silence.py | 4 ++-- tests/bot/cogs/moderation/test_silence.py | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) (limited to 'tests') diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py index 12896022f..178dee06f 100644 --- a/bot/cogs/moderation/silence.py +++ b/bot/cogs/moderation/silence.py @@ -82,9 +82,9 @@ class Silence(commands.Cog): self.bot = bot self.scheduler = Scheduler(self.__class__.__name__) - self._init_task = self.bot.loop.create_task(self._init_cog()) + self._init_task = self.bot.loop.create_task(self._async_init()) - async def _init_cog(self) -> None: + async def _async_init(self) -> None: """Set instance attributes once the guild is available and reschedule unsilences.""" await self.bot.wait_until_guild_available() diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 9dbdfd10a..5588115ae 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -91,39 +91,39 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): self.cog = silence.Silence(self.bot) @autospec(silence, "SilenceNotifier", pass_mocks=False) - async def test_init_cog_got_guild(self): + async def test_async_init_got_guild(self): """Bot got guild after it became available.""" - await self.cog._init_cog() + await self.cog._async_init() self.bot.wait_until_guild_available.assert_awaited_once() self.bot.get_guild.assert_called_once_with(Guild.id) @autospec(silence, "SilenceNotifier", pass_mocks=False) - async def test_init_cog_got_role(self): + async def test_async_init_got_role(self): """Got `Roles.verified` role from guild.""" - await self.cog._init_cog() + await self.cog._async_init() guild = self.bot.get_guild() guild.get_role.assert_called_once_with(Roles.verified) @autospec(silence, "SilenceNotifier", pass_mocks=False) - async def test_init_cog_got_channels(self): + async def test_async_init_got_channels(self): """Got channels from bot.""" - await self.cog._init_cog() + await self.cog._async_init() self.bot.get_channel.called_once_with(Channels.mod_alerts) self.bot.get_channel.called_once_with(Channels.mod_log) @autospec(silence, "SilenceNotifier") - async def test_init_cog_got_notifier(self, notifier): + async def test_async_init_got_notifier(self, notifier): """Notifier was started with channel.""" mod_log = MockTextChannel() self.bot.get_channel.side_effect = (None, mod_log) - await self.cog._init_cog() + await self.cog._async_init() notifier.assert_called_once_with(self.cog._mod_log_channel) @autospec(silence, "SilenceNotifier", pass_mocks=False) - async def test_init_cog_rescheduled(self): + async def test_async_init_rescheduled(self): """`_reschedule_` coroutine was awaited.""" self.cog._reschedule = mock.create_autospec(self.cog._reschedule) - await self.cog._init_cog() + await self.cog._async_init() self.cog._reschedule.assert_awaited_once_with() def test_cog_unload_cancelled_tasks(self): @@ -154,7 +154,7 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): self.cog._unsilence_wrapper = mock.create_autospec(self.cog._unsilence_wrapper) with mock.patch.object(self.cog, "_reschedule", autospec=True): - asyncio.run(self.cog._init_cog()) # Populate instance attributes. + asyncio.run(self.cog._async_init()) # Populate instance attributes. async def test_skipped_missing_channel(self): """Did nothing because the channel couldn't be retrieved.""" @@ -230,7 +230,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.cog._init_task = asyncio.Future() self.cog._init_task.set_result(None) - asyncio.run(self.cog._init_cog()) # Populate instance attributes. + asyncio.run(self.cog._async_init()) # Populate instance attributes. self.channel = MockTextChannel() self.overwrite = PermissionOverwrite(stream=True, send_messages=True, add_reactions=False) @@ -367,7 +367,7 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): overwrites_cache = mock.create_autospec(self.cog.previous_overwrites, spec_set=True) self.cog.previous_overwrites = overwrites_cache - asyncio.run(self.cog._init_cog()) # Populate instance attributes. + asyncio.run(self.cog._async_init()) # Populate instance attributes. self.cog.scheduler.__contains__.return_value = True overwrites_cache.get.return_value = '{"send_messages": true, "add_reactions": false}' -- cgit v1.2.3 From 46bdcdf9414786f1432b4937590a0448122e6f34 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 7 Oct 2020 15:13:01 -0700 Subject: Silence tests: fix unawaited coro warnings Because the Scheduler is mocked, it doesn't actually do anything with the coroutines passed to the schedule() functions, hence the warnings. --- tests/bot/cogs/moderation/test_silence.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 5588115ae..6a8db72e8 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -68,7 +68,9 @@ class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): with self.subTest(current_loop=current_loop): with mock.patch.object(self.notifier, "_current_loop", new=current_loop): await self.notifier._notifier() - self.alert_channel.send.assert_called_once_with(f"<@&{Roles.moderators}> currently silenced channels: ") + self.alert_channel.send.assert_called_once_with( + f"<@&{Roles.moderators}> currently silenced channels: " + ) self.alert_channel.send.reset_mock() async def test_notifier_skips_alert(self): @@ -158,7 +160,7 @@ class RescheduleTests(unittest.IsolatedAsyncioTestCase): async def test_skipped_missing_channel(self): """Did nothing because the channel couldn't be retrieved.""" - self.cog.unsilence_timestamps.items.return_value = [(123, -1), (123, 1), (123, 100000000000)] + self.cog.unsilence_timestamps.items.return_value = [(123, -1), (123, 1), (123, 10000000000)] self.bot.get_channel.return_value = None await self.cog._reschedule() @@ -230,6 +232,9 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.cog._init_task = asyncio.Future() self.cog._init_task.set_result(None) + # Avoid unawaited coroutine warnings. + self.cog.scheduler.schedule_later.side_effect = lambda delay, task_id, coro: coro.close() + asyncio.run(self.cog._async_init()) # Populate instance attributes. self.channel = MockTextChannel() -- cgit v1.2.3 From d0c3990e8eb9e68537c05ec58594abdf5c4cee9e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 8 Oct 2020 12:25:41 -0700 Subject: Silence: add to notifier when indefinite rather than temporary Accidentally swapped the logic in a previous commit during a refactor. --- bot/cogs/moderation/silence.py | 2 +- tests/bot/cogs/moderation/test_silence.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py index 178dee06f..95706392a 100644 --- a/bot/cogs/moderation/silence.py +++ b/bot/cogs/moderation/silence.py @@ -116,10 +116,10 @@ class Silence(commands.Cog): await self._schedule_unsilence(ctx, duration) if duration is None: + self.notifier.add_channel(ctx.channel) log.info(f"Silenced {channel_info} indefinitely.") await ctx.send(MSG_SILENCE_PERMANENT) else: - self.notifier.add_channel(ctx.channel) log.info(f"Silenced {channel_info} for {duration} minute(s).") await ctx.send(MSG_SILENCE_SUCCESS.format(duration=duration)) diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 6a8db72e8..50d8419ac 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -296,17 +296,17 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): self.assertDictEqual(prev_overwrite_dict, new_overwrite_dict) - async def test_temp_added_to_notifier(self): - """Channel was added to notifier if a duration was set for the silence.""" + 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, "_silence_overwrites", return_value=True): await self.cog.silence.callback(self.cog, MockContext(), 15) - self.cog.notifier.add_channel.assert_called_once() + self.cog.notifier.add_channel.assert_not_called() - async def test_indefinite_not_added_to_notifier(self): - """Channel was not added to notifier if a duration was not set for the silence.""" + 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, "_silence_overwrites", return_value=True): await self.cog.silence.callback(self.cog, MockContext(), None) - self.cog.notifier.add_channel.assert_not_called() + self.cog.notifier.add_channel.assert_called_once() async def test_silenced_not_added_to_notifier(self): """Channel was not added to the notifier if it was already silenced.""" -- cgit v1.2.3 From 5b87a272ff21df9fa4fb59fdf9ec92c6b57193c6 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 8 Oct 2020 13:22:54 -0700 Subject: Silence: remove _mod_log_channel attribute It's only used as an argument to `SilenceNotifier`, so it doesn't need to be an instance attribute. --- bot/cogs/moderation/silence.py | 3 +-- tests/bot/cogs/moderation/test_silence.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py index 95706392a..80c4e6a25 100644 --- a/bot/cogs/moderation/silence.py +++ b/bot/cogs/moderation/silence.py @@ -91,8 +91,7 @@ class Silence(commands.Cog): guild = self.bot.get_guild(Guild.id) self._verified_role = guild.get_role(Roles.verified) self._mod_alerts_channel = self.bot.get_channel(Channels.mod_alerts) - self._mod_log_channel = self.bot.get_channel(Channels.mod_log) - self.notifier = SilenceNotifier(self._mod_log_channel) + self.notifier = SilenceNotifier(self.bot.get_channel(Channels.mod_log)) await self._reschedule() @commands.command(aliases=("hush",)) diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 50d8419ac..6f8f4386b 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -119,7 +119,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): mod_log = MockTextChannel() self.bot.get_channel.side_effect = (None, mod_log) await self.cog._async_init() - notifier.assert_called_once_with(self.cog._mod_log_channel) + notifier.assert_called_once_with(mod_log) @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_async_init_rescheduled(self): -- cgit v1.2.3 From e85a4d254cadd303537a4d2cce6637bbcd3cf2f9 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 8 Oct 2020 13:23:35 -0700 Subject: Silence tests: make _async_init attribute tests more robust --- tests/bot/cogs/moderation/test_silence.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/tests/bot/cogs/moderation/test_silence.py b/tests/bot/cogs/moderation/test_silence.py index 6f8f4386b..3e1b963b0 100644 --- a/tests/bot/cogs/moderation/test_silence.py +++ b/tests/bot/cogs/moderation/test_silence.py @@ -102,24 +102,28 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_async_init_got_role(self): """Got `Roles.verified` role from guild.""" - await self.cog._async_init() guild = self.bot.get_guild() - guild.get_role.assert_called_once_with(Roles.verified) + guild.get_role.side_effect = lambda id_: Mock(id=id_) + + await self.cog._async_init() + self.assertEqual(self.cog._verified_role.id, Roles.verified) @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_async_init_got_channels(self): """Got channels from bot.""" + self.bot.get_channel.side_effect = lambda id_: MockTextChannel(id=id_) + await self.cog._async_init() - self.bot.get_channel.called_once_with(Channels.mod_alerts) - self.bot.get_channel.called_once_with(Channels.mod_log) + self.assertEqual(self.cog._mod_alerts_channel.id, Channels.mod_alerts) @autospec(silence, "SilenceNotifier") async def test_async_init_got_notifier(self, notifier): """Notifier was started with channel.""" - mod_log = MockTextChannel() - self.bot.get_channel.side_effect = (None, mod_log) + self.bot.get_channel.side_effect = lambda id_: MockTextChannel(id=id_) + await self.cog._async_init() - notifier.assert_called_once_with(mod_log) + notifier.assert_called_once_with(MockTextChannel(id=Channels.mod_log)) + self.assertEqual(self.cog.notifier, notifier.return_value) @autospec(silence, "SilenceNotifier", pass_mocks=False) async def test_async_init_rescheduled(self): -- cgit v1.2.3 From bfab4928e5b219660f76e2516c4ec0bb67fcba89 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 9 Oct 2020 18:26:03 -0700 Subject: Silence: require only 1 permission to be False for a manual unsilence Previously, both sending messages and adding reactions had to be false in order for the manual unsilence failure message to be sent. Because staff may only set one of these manually, the message should be sent if at least one of the permissions is set. --- bot/exts/moderation/silence.py | 2 +- tests/bot/exts/moderation/test_silence.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index bb8e06924..ee2c0dc7c 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -136,7 +136,7 @@ class Silence(commands.Cog): """Unsilence `channel` and send a success/failure message.""" if not await self._unsilence(channel): overwrite = channel.overwrites_for(self._verified_role) - if overwrite.send_messages is False and overwrite.add_reactions is False: + if overwrite.send_messages is False or overwrite.add_reactions is False: await channel.send(MSG_UNSILENCE_MANUAL) else: await channel.send(MSG_UNSILENCE_FAIL) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 39e32fdb2..6d5ffa7e8 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -411,6 +411,8 @@ class UnsilenceTests(unittest.IsolatedAsyncioTestCase): (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, PermissionOverwrite(send_messages=False)), + (False, silence.MSG_UNSILENCE_MANUAL, PermissionOverwrite(add_reactions=False)), ) for was_unsilenced, message, overwrite in test_cases: ctx = MockContext() -- cgit v1.2.3 From dc2797bc7d24d80c08c559bc381c465db1312143 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 18 Oct 2020 18:12:29 -0700 Subject: Silence: rename function to reduce ambiguity --- bot/exts/moderation/silence.py | 4 ++-- tests/bot/exts/moderation/test_silence.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) (limited to 'tests') diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index cfdefe103..3bbf8d21a 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -107,7 +107,7 @@ class Silence(commands.Cog): channel_info = f"#{ctx.channel} ({ctx.channel.id})" log.debug(f"{ctx.author} is silencing channel {channel_info}.") - if not await self._silence_overwrites(ctx.channel): + if not await self._set_silence_overwrites(ctx.channel): log.info(f"Tried to silence channel {channel_info} but the channel was already silenced.") await ctx.send(MSG_SILENCE_FAIL) return @@ -144,7 +144,7 @@ class Silence(commands.Cog): else: await channel.send(MSG_UNSILENCE_SUCCESS) - async def _silence_overwrites(self, channel: TextChannel) -> bool: + async def _set_silence_overwrites(self, channel: TextChannel) -> bool: """Set silence permission overwrites for `channel` and return True if successful.""" overwrite = channel.overwrites_for(self._verified_role) prev_overwrites = dict(send_messages=overwrite.send_messages, add_reactions=overwrite.add_reactions) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 6d5ffa7e8..6b67a21a0 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -274,7 +274,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): ) for duration, message, was_silenced in test_cases: ctx = MockContext() - with mock.patch.object(self.cog, "_silence_overwrites", return_value=was_silenced): + 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) @@ -293,12 +293,12 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): channel = MockTextChannel() channel.overwrites_for.return_value = overwrite - self.assertFalse(await self.cog._silence_overwrites(channel)) + self.assertFalse(await self.cog._set_silence_overwrites(channel)) channel.set_permissions.assert_not_called() async def test_silenced_channel(self): """Channel had `send_message` and `add_reactions` permissions revoked for verified role.""" - self.assertTrue(await self.cog._silence_overwrites(self.channel)) + 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( @@ -309,7 +309,7 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): async def test_preserved_other_overwrites(self): """Channel's other unrelated overwrites were not changed.""" prev_overwrite_dict = dict(self.overwrite) - await self.cog._silence_overwrites(self.channel) + await self.cog._set_silence_overwrites(self.channel) new_overwrite_dict = dict(self.overwrite) # Remove 'send_messages' & 'add_reactions' keys because they were changed by the method. @@ -322,26 +322,26 @@ class SilenceTests(unittest.IsolatedAsyncioTestCase): 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, "_silence_overwrites", return_value=True): + with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=True): await self.cog.silence.callback(self.cog, MockContext(), 15) self.cog.notifier.add_channel.assert_not_called() 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, "_silence_overwrites", return_value=True): + with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=True): await self.cog.silence.callback(self.cog, MockContext(), None) self.cog.notifier.add_channel.assert_called_once() async def test_silenced_not_added_to_notifier(self): """Channel was not added to the notifier if it was already silenced.""" - with mock.patch.object(self.cog, "_silence_overwrites", return_value=False): + with mock.patch.object(self.cog, "_set_silence_overwrites", return_value=False): await self.cog.silence.callback(self.cog, MockContext(), 15) self.cog.notifier.add_channel.assert_not_called() async def test_cached_previous_overwrites(self): """Channel's previous overwrites were cached.""" overwrite_json = '{"send_messages": true, "add_reactions": false}' - await self.cog._silence_overwrites(self.channel) + await self.cog._set_silence_overwrites(self.channel) self.cog.previous_overwrites.set.assert_called_once_with(self.channel.id, overwrite_json) @autospec(silence, "datetime") -- cgit v1.2.3 From 50324b3ec0e0285300e4f4cf389fd93c4801f1ec Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 21 Oct 2020 09:46:33 -0700 Subject: Silence tests: update docstrings in notifier tests --- tests/bot/exts/moderation/test_silence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 6b67a21a0..104293d8e 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -43,7 +43,7 @@ class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): self.notifier.start = self.notifier_start_mock = Mock() def test_add_channel_adds_channel(self): - """Channel in FirstHash with current loop is added to internal set.""" + """Channel is added to `_silenced_channels` with the current loop.""" channel = Mock() with mock.patch.object(self.notifier, "_silenced_channels") as silenced_channels: self.notifier.add_channel(channel) @@ -61,7 +61,7 @@ class SilenceNotifierTests(unittest.IsolatedAsyncioTestCase): self.notifier_start_mock.assert_not_called() def test_remove_channel_removes_channel(self): - """Channel in FirstHash is removed from `_silenced_channels`.""" + """Channel is removed from `_silenced_channels`.""" channel = Mock() with mock.patch.object(self.notifier, "_silenced_channels") as silenced_channels: self.notifier.remove_channel(channel) -- cgit v1.2.3