diff options
-rw-r--r-- | pydis_site/apps/api/models/__init__.py | 1 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_filters.py | 309 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_models.py | 3 |
3 files changed, 170 insertions, 143 deletions
diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 580c95a0..fee4c8d5 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -6,7 +6,6 @@ from .bot import ( BumpedThread, DocumentationLink, DeletedMessage, - FilterList, Infraction, Message, MessageDeletionContext, diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py index f3afdaeb..cae78cd6 100644 --- a/pydis_site/apps/api/tests/test_filters.py +++ b/pydis_site/apps/api/tests/test_filters.py @@ -1,19 +1,13 @@ import contextlib from dataclasses import dataclass +from datetime import timedelta from typing import Any, Dict, Tuple, Type from django.db.models import Model -from django_hosts import reverse +from django.urls import reverse -from pydis_site.apps.api.models.bot.filters import ( # noqa: I101 - Preserving the filter order - FilterList, - FilterSettings, - FilterAction, - ChannelRange, - Filter, - FilterOverride -) -from pydis_site.apps.api.tests.base import APISubdomainTestCase +from pydis_site.apps.api.models.bot.filters import FilterList, Filter +from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase @dataclass() @@ -21,99 +15,76 @@ class TestSequence: model: Type[Model] route: str object: Dict[str, Any] - ignored_fields: Tuple[str] = () + ignored_fields: Tuple[str, ...] = () def url(self, detail: bool = False) -> str: - return reverse(f'bot:{self.route}-{"detail" if detail else "list"}', host='api') + return reverse(f'api:bot:{self.route}-{"detail" if detail else "list"}') -FK_FIELDS: Dict[Type[Model], Tuple[str]] = { - FilterList: ("default_settings",), - FilterSettings: ("default_action", "default_range"), - FilterAction: (), - ChannelRange: (), +FK_FIELDS: Dict[Type[Model], Tuple[str, ...]] = { + FilterList: (), Filter: ("filter_list",), - FilterOverride: ("filter_action", "filter_range") } def get_test_sequences() -> Dict[str, TestSequence]: + filter_list1_deny_dict = { + "name": "testname", + "list_type": 0, + "guild_pings": [], + "filter_dm": True, + "dm_pings": [], + "remove_context": False, + "bypass_roles": [], + "enabled": True, + "dm_content": "", + "dm_embed": "", + "infraction_type": "NONE", + "infraction_reason": "", + "infraction_duration": timedelta(seconds=0), + "infraction_channel": 0, + "disabled_channels": [], + "disabled_categories": [], + "enabled_channels": [], + "enabled_categories": [], + "send_alert": True + } + filter_list1_allow_dict = filter_list1_deny_dict.copy() + filter_list1_allow_dict["list_type"] = 1 + filter_list1_allow = FilterList(**filter_list1_allow_dict) + return { - "filter_list": TestSequence( + "filter_list1": TestSequence( FilterList, "filterlist", - { - "name": "testname", - "list_type": 0, - "default_settings": FilterSettings( - ping_type=[], - filter_dm=False, - dm_ping_type=[], - remove_context=False, - bypass_roles=[], - enabled=False, - default_action=FilterAction( - dm_content=None, - infraction_type=None, - infraction_reason="", - infraction_duration=None - ), - default_range=ChannelRange( - disallowed_channels=[], - disallowed_categories=[], - allowed_channels=[], - allowed_categories=[], - default=False - ) - ) - }, - ignored_fields=("filters",) + filter_list1_deny_dict, + ignored_fields=("filters", "created_at", "updated_at") ), - "filter_settings": TestSequence( - FilterSettings, - "filtersettings", + "filter_list2": TestSequence( + FilterList, + "filterlist", { - "ping_type": ["onduty"], - "filter_dm": True, - "dm_ping_type": ["123456"], + "name": "testname2", + "list_type": 1, + "guild_pings": ["Moderators"], + "filter_dm": False, + "dm_pings": ["here"], "remove_context": True, - "bypass_roles": [123456], - "enabled": True, - "default_action": FilterAction( - dm_content=None, - infraction_type=None, - infraction_reason="", - infraction_duration=None - ), - "default_range": ChannelRange( - disallowed_channels=[], - disallowed_categories=[], - allowed_channels=[], - allowed_categories=[], - default=False - ) - } - ), - "filter_action": TestSequence( - FilterAction, - "filteraction", - { - "dm_content": "This is a DM message.", - "infraction_type": "Mute", - "infraction_reason": "Too long beard", - "infraction_duration": "1 02:03:00" - } - ), - "channel_range": TestSequence( - ChannelRange, - "channelrange", - { - "disallowed_channels": [1234], - "disallowed_categories": [5678], - "allowed_channels": [9101], - "allowed_categories": [1121], - "default": True - } + "bypass_roles": ["123456"], + "enabled": False, + "dm_content": "testing testing", + "dm_embed": "one two three", + "infraction_type": "MUTE", + "infraction_reason": "stop testing", + "infraction_duration": timedelta(seconds=10.5), + "infraction_channel": 123, + "disabled_channels": ["python-general"], + "disabled_categories": ["CODE JAM"], + "enabled_channels": ["mighty-mice"], + "enabled_categories": ["Lobby"], + "send_alert": False + }, + ignored_fields=("filters", "created_at", "updated_at") ), "filter": TestSequence( Filter, @@ -121,58 +92,35 @@ def get_test_sequences() -> Dict[str, TestSequence]: { "content": "bad word", "description": "This is a really bad word.", - "additional_field": None, - "override": None, - "filter_list": FilterList( - name="testname", - list_type=0, - default_settings=FilterSettings( - ping_type=[], - filter_dm=False, - dm_ping_type=[], - remove_context=False, - bypass_roles=[], - enabled=False, - default_action=FilterAction( - dm_content=None, - infraction_type=None, - infraction_reason="", - infraction_duration=None - ), - default_range=ChannelRange( - disallowed_channels=[], - disallowed_categories=[], - allowed_channels=[], - allowed_categories=[], - default=False - ) - ) - ) - } + "additional_field": "{'hi': 'there'}", + "guild_pings": None, + "filter_dm": None, + "dm_pings": None, + "remove_context": None, + "bypass_roles": None, + "enabled": None, + "dm_content": None, + "dm_embed": None, + "infraction_type": None, + "infraction_reason": None, + "infraction_duration": None, + "infraction_channel": None, + "disabled_channels": None, + "disabled_categories": None, + "enabled_channels": None, + "enabled_categories": None, + "send_alert": None, + "filter_list": filter_list1_allow + }, + ignored_fields=("created_at", "updated_at") ), - "filter_override": TestSequence( - FilterOverride, - "filteroverride", - { - "ping_type": ["everyone"], - "filter_dm": False, - "dm_ping_type": ["here"], - "remove_context": False, - "bypass_roles": [9876], - "enabled": True, - "filter_action": None, - "filter_range": None - } - ) } def save_nested_objects(object_: Model, save_root: bool = True) -> None: - for field in FK_FIELDS[object_.__class__]: + for field in FK_FIELDS.get(object_.__class__, ()): value = getattr(object_, field) - - if value is not None: - save_nested_objects(value) + save_nested_objects(value) if save_root: object_.save() @@ -182,6 +130,8 @@ def clean_test_json(json: dict) -> dict: for key, value in json.items(): if isinstance(value, Model): json[key] = value.id + elif isinstance(value, timedelta): + json[key] = str(value.total_seconds()) return json @@ -194,7 +144,22 @@ def clean_api_json(json: dict, sequence: TestSequence) -> dict: return json -class GenericFilterTest(APISubdomainTestCase): +def flatten_settings(json: dict) -> dict: + settings = json.pop("settings", {}) + flattened_settings = {} + for entry, value in settings.items(): + if isinstance(value, dict): + flattened_settings.update(value) + else: + flattened_settings[entry] = value + + json.update(flattened_settings) + + return json + + +class GenericFilterTests(AuthenticatedAPITestCase): + def test_cannot_read_unauthenticated(self) -> None: for name, sequence in get_test_sequences().items(): with self.subTest(name=name): @@ -222,7 +187,7 @@ class GenericFilterTest(APISubdomainTestCase): response = self.client.get(sequence.url()) self.assertDictEqual( clean_test_json(sequence.object), - clean_api_json(response.json()[0], sequence) + clean_api_json(flatten_settings(response.json()[0]), sequence) ) def test_fetch_by_id(self) -> None: @@ -236,7 +201,7 @@ class GenericFilterTest(APISubdomainTestCase): response = self.client.get(f"{sequence.url()}/{saved.id}") self.assertDictEqual( clean_test_json(sequence.object), - clean_api_json(response.json(), sequence) + clean_api_json(flatten_settings(response.json()), sequence) ) def test_fetch_non_existing(self) -> None: @@ -259,14 +224,15 @@ class GenericFilterTest(APISubdomainTestCase): self.assertEqual(response.status_code, 201) self.assertDictEqual( - clean_api_json(response.json(), sequence), + clean_api_json(flatten_settings(response.json()), sequence), clean_test_json(sequence.object) ) def test_creation_missing_field(self) -> None: for name, sequence in get_test_sequences().items(): with self.subTest(name=name): - save_nested_objects(sequence.model(**sequence.object), False) + saved = sequence.model(**sequence.object) + save_nested_objects(saved) data = clean_test_json(sequence.object.copy()) for field in sequence.model._meta.get_fields(): @@ -296,3 +262,68 @@ class GenericFilterTest(APISubdomainTestCase): response = self.client.delete(f"{sequence.url()}/42") self.assertEqual(response.status_code, 404) + + +class FilterValidationTests(AuthenticatedAPITestCase): + + def test_filter_validation(self) -> None: + test_sequences = get_test_sequences() + base_filter = test_sequences["filter"] + base_filter_list = test_sequences["filter_list1"] + cases = ( + ({"infraction_reason": "hi"}, {}, 400), ({"infraction_duration": timedelta(seconds=10)}, {}, 400), + ({"infraction_reason": "hi"}, {"infraction_type": "NOTE"}, 200), + ({"infraction_duration": timedelta(seconds=10)}, {"infraction_type": "MUTE"}, 200), + ({"enabled_channels": ["admins"]}, {}, 200), ({"disabled_channels": ["123"]}, {}, 200), + ({"enabled_categories": ["CODE JAM"]}, {}, 200), ({"disabled_categories": ["CODE JAM"]}, {}, 200), + ({"enabled_channels": ["admins"], "disabled_channels": ["123", "admins"]}, {}, 400), + ({"enabled_categories": ["admins"], "disabled_categories": ["123", "admins"]}, {}, 400), + ({"enabled_channels": ["admins"]}, {"disabled_channels": ["123", "admins"]}, 400), + ({"enabled_categories": ["admins"]}, {"disabled_categories": ["123", "admins"]}, 400), + ) + + for filter_settings, filter_list_settings, response_code in cases: + with self.subTest(f_settings=filter_settings, fl_settings=filter_list_settings, response=response_code): + base_filter.model.objects.all().delete() + base_filter_list.model.objects.all().delete() + + case_filter_dict = base_filter.object.copy() + case_fl_dict = base_filter_list.object.copy() + case_fl_dict.update(filter_list_settings) + + case_fl = base_filter_list.model(**case_fl_dict) + case_filter_dict["filter_list"] = case_fl + case_filter = base_filter.model(**case_filter_dict) + save_nested_objects(case_filter) + + filter_settings["filter_list"] = case_fl + response = self.client.patch( + f"{base_filter.url()}/{case_filter.id}", data=clean_test_json(filter_settings) + ) + self.assertEqual(response.status_code, response_code) + + def test_filter_list_validation(self) -> None: + test_sequences = get_test_sequences() + base_filter_list = test_sequences["filter_list1"] + cases = ( + ({"infraction_reason": "hi"}, 400), ({"infraction_duration": timedelta(seconds=10)}, 400), + ({"infraction_reason": "hi", "infraction_type": "NOTE"}, 200), + ({"infraction_duration": timedelta(seconds=10), "infraction_type": "MUTE"}, 200), + ({"enabled_channels": ["admins"]}, 200), ({"disabled_channels": ["123"]}, 200), + ({"enabled_categories": ["CODE JAM"]}, 200), ({"disabled_categories": ["CODE JAM"]}, 200), + ({"enabled_channels": ["admins"], "disabled_channels": ["123", "admins"]}, 400), + ({"enabled_categories": ["admins"], "disabled_categories": ["123", "admins"]}, 400), + ) + + for filter_list_settings, response_code in cases: + with self.subTest(fl_settings=filter_list_settings, response=response_code): + base_filter_list.model.objects.all().delete() + + case_fl_dict = base_filter_list.object.copy() + case_fl = base_filter_list.model(**case_fl_dict) + save_nested_objects(case_fl) + + response = self.client.patch( + f"{base_filter_list.url()}/{case_fl.id}", data=clean_test_json(filter_list_settings) + ) + self.assertEqual(response.status_code, response_code) diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index b9b14a84..25d771cc 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -8,7 +8,6 @@ from pydis_site.apps.api.models import ( DocumentationLink, Filter, FilterList, - FilterSettings, Infraction, MessageDeletionContext, Nomination, @@ -110,13 +109,11 @@ class StringDunderMethodTests(SimpleTestCase): FilterList( name="forbidden_duckies", list_type=0, - default_settings=FilterSettings() ), Filter( content="ducky_nsfw", description="This ducky is totally inappropriate!", additional_field=None, - override=None ), OffensiveMessage( id=602951077675139072, |