aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorGravatar mbaruh <[email protected]>2021-12-09 00:30:41 +0200
committerGravatar mbaruh <[email protected]>2022-07-16 01:45:39 +0300
commit8095800ae8f38928ab8c406e622ec79ea93b21c3 (patch)
tree519976086843c54e762388715a30a349fd722294 /tests
parentTear down the old filtering system (diff)
New filtering backbone and regex filtering migration
This commit provides the basis of the new filtering system: - The filtering cog consists of several filter lists loaded from the database (filtering.py). - Each filter list contains a list of filters, which are run in response to events (message posting, reaction, thread creation). Each filter list may choose to respond to different events (the subscribe method in filtering.py). - Each filter has settings (settings.py) which decide when it is going to be run (e.g it might be disabled in a specific channel), and what will happen if it triggers (e.g delete the offending message). - Not every filter has a value for every setting (the _settings_types package) . It will use the default settings specified by its filter list as a fallback. - Since each filter might have a different effect when triggered, we must check all relevant filters even if we found a triggered filter already, unlike in the old system. - Two triggered filters may specify different values for the same setting, therefore each entry has a rule for combining two different values (the __or__ method in each file in _settings_types). To avoid having to prefix each file with an underscore (or the bot will try to load it as a cog), the loading script was changed to ignore packages with names starting with an underscore. Alert sending is done via a webhook so that several embeds can be sent in the same message (will be useful for example for guild invite alerts). Filter lists and setting entries classes are loaded dynamically from their respective packages. In order to be able to test the new features, this commit also includes a migration of the regex-based filtering.
Diffstat (limited to 'tests')
-rw-r--r--tests/bot/exts/filtering/__init__.py0
-rw-r--r--tests/bot/exts/filtering/test_filters.py41
-rw-r--r--tests/bot/exts/filtering/test_settings.py20
-rw-r--r--tests/bot/exts/filtering/test_settings_entries.py272
4 files changed, 333 insertions, 0 deletions
diff --git a/tests/bot/exts/filtering/__init__.py b/tests/bot/exts/filtering/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/bot/exts/filtering/__init__.py
diff --git a/tests/bot/exts/filtering/test_filters.py b/tests/bot/exts/filtering/test_filters.py
new file mode 100644
index 000000000..214637b52
--- /dev/null
+++ b/tests/bot/exts/filtering/test_filters.py
@@ -0,0 +1,41 @@
+import unittest
+
+from bot.exts.filtering._filter_context import Event, FilterContext
+from bot.exts.filtering._filters.token import TokenFilter
+from tests.helpers import MockMember, MockMessage, MockTextChannel
+
+
+class FilterTests(unittest.TestCase):
+ """Test functionality of the token filter."""
+
+ def setUp(self) -> None:
+ member = MockMember(id=123)
+ channel = MockTextChannel(id=345)
+ message = MockMessage(author=member, channel=channel)
+ self.ctx = FilterContext(Event.MESSAGE, member, channel, "", message)
+
+ def test_token_filter_triggers(self):
+ """The filter should evaluate to True only if its token is found in the context content."""
+ test_cases = (
+ (r"hi", "oh hi there", True),
+ (r"hi", "goodbye", False),
+ (r"bla\d{2,4}", "bla18", True),
+ (r"bla\d{2,4}", "bla1", False)
+ )
+
+ for pattern, content, expected in test_cases:
+ with self.subTest(
+ pattern=pattern,
+ content=content,
+ expected=expected,
+ ):
+ filter_ = TokenFilter({
+ "id": 1,
+ "content": pattern,
+ "description": None,
+ "settings": {},
+ "additional_field": "{}" # noqa: P103
+ })
+ self.ctx.content = content
+ result = filter_.triggered_on(self.ctx)
+ self.assertEqual(result, expected)
diff --git a/tests/bot/exts/filtering/test_settings.py b/tests/bot/exts/filtering/test_settings.py
new file mode 100644
index 000000000..ac21a5d47
--- /dev/null
+++ b/tests/bot/exts/filtering/test_settings.py
@@ -0,0 +1,20 @@
+import unittest
+
+import bot.exts.filtering._settings
+from bot.exts.filtering._settings import create_settings
+
+
+class FilterTests(unittest.TestCase):
+ """Test functionality of the Settings class and its subclasses."""
+
+ def test_create_settings_returns_none_for_empty_data(self):
+ """`create_settings` should return a tuple of two Nones when passed an empty dict."""
+ result = create_settings({})
+
+ self.assertEquals(result, (None, None))
+
+ def test_unrecognized_entry_makes_a_warning(self):
+ """When an unrecognized entry name is passed to `create_settings`, it should be added to `_already_warned`."""
+ create_settings({"abcd": {}})
+
+ self.assertIn("abcd", bot.exts.filtering._settings._already_warned)
diff --git a/tests/bot/exts/filtering/test_settings_entries.py b/tests/bot/exts/filtering/test_settings_entries.py
new file mode 100644
index 000000000..4db6438ab
--- /dev/null
+++ b/tests/bot/exts/filtering/test_settings_entries.py
@@ -0,0 +1,272 @@
+import unittest
+
+from bot.exts.filtering._filter_context import Event, FilterContext
+from bot.exts.filtering._settings_types.bypass_roles import RoleBypass
+from bot.exts.filtering._settings_types.channel_scope import ChannelScope
+from bot.exts.filtering._settings_types.filter_dm import FilterDM
+from bot.exts.filtering._settings_types.infraction_and_notification import (
+ Infraction, InfractionAndNotification, superstar
+)
+from tests.helpers import MockCategoryChannel, MockDMChannel, MockMember, MockMessage, MockRole, MockTextChannel
+
+
+class FilterTests(unittest.TestCase):
+ """Test functionality of the Settings class and its subclasses."""
+
+ def setUp(self) -> None:
+ member = MockMember(id=123)
+ channel = MockTextChannel(id=345)
+ message = MockMessage(author=member, channel=channel)
+ self.ctx = FilterContext(Event.MESSAGE, member, channel, "", message)
+
+ def test_role_bypass_is_off_for_user_without_roles(self):
+ """The role bypass should trigger when a user has no roles."""
+ member = MockMember()
+ self.ctx.author = member
+ bypass_entry = RoleBypass(["123"])
+
+ result = bypass_entry.triggers_on(self.ctx)
+
+ self.assertTrue(result)
+
+ def test_role_bypass_is_on_for_a_user_with_the_right_role(self):
+ """The role bypass should not trigger when the user has one of its roles."""
+ cases = (
+ ([123], ["123"]),
+ ([123, 234], ["123"]),
+ ([123], ["123", "234"]),
+ ([123, 234], ["123", "234"])
+ )
+
+ for user_role_ids, bypasses in cases:
+ with self.subTest(user_role_ids=user_role_ids, bypasses=bypasses):
+ user_roles = [MockRole(id=role_id) for role_id in user_role_ids]
+ member = MockMember(roles=user_roles)
+ self.ctx.author = member
+ bypass_entry = RoleBypass(bypasses)
+
+ result = bypass_entry.triggers_on(self.ctx)
+
+ self.assertFalse(result)
+
+ def test_context_doesnt_trigger_for_empty_channel_scope(self):
+ """A filter is enabled for all channels by default."""
+ channel = MockTextChannel()
+ scope = ChannelScope({"disabled_channels": None, "disabled_categories": None, "enabled_channels": None})
+ self.ctx.channel = channel
+
+ result = scope.triggers_on(self.ctx)
+
+ self.assertTrue(result)
+
+ def test_context_doesnt_trigger_for_disabled_channel(self):
+ """A filter shouldn't trigger if it's been disabled in the channel."""
+ channel = MockTextChannel(id=123)
+ scope = ChannelScope({"disabled_channels": [123], "disabled_categories": None, "enabled_channels": None})
+ self.ctx.channel = channel
+
+ result = scope.triggers_on(self.ctx)
+
+ self.assertFalse(result)
+
+ def test_context_doesnt_trigger_in_disabled_category(self):
+ """A filter shouldn't trigger if it's been disabled in the category."""
+ channel = MockTextChannel()
+ scope = ChannelScope({
+ "disabled_channels": None, "disabled_categories": [channel.category.id], "enabled_channels": None
+ })
+ self.ctx.channel = channel
+
+ result = scope.triggers_on(self.ctx)
+
+ self.assertFalse(result)
+
+ def test_context_triggers_in_enabled_channel_in_disabled_category(self):
+ """A filter should trigger in an enabled channel even if it's been disabled in the category."""
+ channel = MockTextChannel(id=123, category=MockCategoryChannel(id=234))
+ scope = ChannelScope({"disabled_channels": None, "disabled_categories": [234], "enabled_channels": [123]})
+ self.ctx.channel = channel
+
+ result = scope.triggers_on(self.ctx)
+
+ self.assertTrue(result)
+
+ def test_filtering_dms_when_necessary(self):
+ """A filter correctly ignores or triggers in a channel depending on the value of FilterDM."""
+ cases = (
+ (True, MockDMChannel(), True),
+ (False, MockDMChannel(), False),
+ (True, MockTextChannel(), True),
+ (False, MockTextChannel(), True)
+ )
+
+ for apply_in_dms, channel, expected in cases:
+ with self.subTest(apply_in_dms=apply_in_dms, channel=channel):
+ filter_dms = FilterDM(apply_in_dms)
+ self.ctx.channel = channel
+
+ result = filter_dms.triggers_on(self.ctx)
+
+ self.assertEqual(expected, result)
+
+ def test_infraction_merge_of_same_infraction_type(self):
+ """When both infractions are of the same type, the one with the longer duration wins."""
+ infraction1 = InfractionAndNotification({
+ "infraction_type": "mute",
+ "infraction_reason": "hi",
+ "infraction_duration": 10,
+ "dm_content": "how",
+ "dm_embed": "what is"
+ })
+ infraction2 = InfractionAndNotification({
+ "infraction_type": "mute",
+ "infraction_reason": "there",
+ "infraction_duration": 20,
+ "dm_content": "are you",
+ "dm_embed": "your name"
+ })
+
+ result = infraction1 | infraction2
+
+ self.assertDictEqual(
+ result.to_dict(),
+ {
+ "infraction_type": Infraction.MUTE,
+ "infraction_reason": "there",
+ "infraction_duration": 20.0,
+ "dm_content": "are you",
+ "dm_embed": "your name",
+ "_superstar": None
+ }
+ )
+
+ def test_infraction_merge_of_different_infraction_types(self):
+ """If there are two different infraction types, the one higher up the hierarchy should be picked."""
+ infraction1 = InfractionAndNotification({
+ "infraction_type": "mute",
+ "infraction_reason": "hi",
+ "infraction_duration": 20,
+ "dm_content": "",
+ "dm_embed": ""
+ })
+ infraction2 = InfractionAndNotification({
+ "infraction_type": "ban",
+ "infraction_reason": "",
+ "infraction_duration": 10,
+ "dm_content": "there",
+ "dm_embed": ""
+ })
+
+ result = infraction1 | infraction2
+
+ self.assertDictEqual(
+ result.to_dict(),
+ {
+ "infraction_type": Infraction.BAN,
+ "infraction_reason": "",
+ "infraction_duration": 10.0,
+ "dm_content": "there",
+ "dm_embed": "",
+ "_superstar": None
+ }
+ )
+
+ def test_infraction_merge_with_a_superstar(self):
+ """If there is a superstar infraction, it should be added to a separate field."""
+ infraction1 = InfractionAndNotification({
+ "infraction_type": "mute",
+ "infraction_reason": "hi",
+ "infraction_duration": 20,
+ "dm_content": "there",
+ "dm_embed": ""
+ })
+ infraction2 = InfractionAndNotification({
+ "infraction_type": "superstar",
+ "infraction_reason": "hello",
+ "infraction_duration": 10,
+ "dm_content": "you",
+ "dm_embed": ""
+ })
+
+ result = infraction1 | infraction2
+
+ self.assertDictEqual(
+ result.to_dict(),
+ {
+ "infraction_type": Infraction.MUTE,
+ "infraction_reason": "hi",
+ "infraction_duration": 20.0,
+ "dm_content": "there",
+ "dm_embed": "",
+ "_superstar": superstar("hello", 10.0)
+ }
+ )
+
+ def test_merge_two_superstar_infractions(self):
+ """When two superstar infractions are merged, the infraction type remains a superstar."""
+ infraction1 = InfractionAndNotification({
+ "infraction_type": "superstar",
+ "infraction_reason": "hi",
+ "infraction_duration": 20,
+ "dm_content": "",
+ "dm_embed": ""
+ })
+ infraction2 = InfractionAndNotification({
+ "infraction_type": "superstar",
+ "infraction_reason": "",
+ "infraction_duration": 10,
+ "dm_content": "there",
+ "dm_embed": ""
+ })
+
+ result = infraction1 | infraction2
+
+ self.assertDictEqual(
+ result.to_dict(),
+ {
+ "infraction_type": Infraction.SUPERSTAR,
+ "infraction_reason": "hi",
+ "infraction_duration": 20.0,
+ "dm_content": "",
+ "dm_embed": "",
+ "_superstar": None
+ }
+ )
+
+ def test_merge_a_voiceban_and_a_superstar_with_another_superstar(self):
+ """An infraction with a superstar merged with a superstar should combine under `_superstar`."""
+ infraction1 = InfractionAndNotification({
+ "infraction_type": "voice ban",
+ "infraction_reason": "hi",
+ "infraction_duration": 20,
+ "dm_content": "hello",
+ "dm_embed": ""
+ })
+ infraction2 = InfractionAndNotification({
+ "infraction_type": "superstar",
+ "infraction_reason": "bla",
+ "infraction_duration": 10,
+ "dm_content": "there",
+ "dm_embed": ""
+ })
+ infraction3 = InfractionAndNotification({
+ "infraction_type": "superstar",
+ "infraction_reason": "blabla",
+ "infraction_duration": 20,
+ "dm_content": "there",
+ "dm_embed": ""
+ })
+
+ result = infraction1 | infraction2 | infraction3
+
+ self.assertDictEqual(
+ result.to_dict(),
+ {
+ "infraction_type": Infraction.VOICE_BAN,
+ "infraction_reason": "hi",
+ "infraction_duration": 20,
+ "dm_content": "hello",
+ "dm_embed": "",
+ "_superstar": superstar("blabla", 20)
+ }
+ )