aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps/api
diff options
context:
space:
mode:
authorGravatar Matteo Bertucci <[email protected]>2021-04-27 16:39:00 +0200
committerGravatar D0rs4n <[email protected]>2021-12-18 18:02:08 +0100
commitc6dfd896304cb4e36c4020f4704d9537fd3e8e9f (patch)
tree3d5a3dbfada12b848ef80f1278c9afec26d64f10 /pydis_site/apps/api
parentFilters: hook the new models into the REST API (diff)
Filters: update tests to the new schema
Diffstat (limited to 'pydis_site/apps/api')
-rw-r--r--pydis_site/apps/api/migrations/0070_new_filter_schema.py2
-rw-r--r--pydis_site/apps/api/models/bot/filters.py2
-rw-r--r--pydis_site/apps/api/tests/test_filterlists.py122
-rw-r--r--pydis_site/apps/api/tests/test_filters.py284
-rw-r--r--pydis_site/apps/api/tests/test_models.py14
5 files changed, 300 insertions, 124 deletions
diff --git a/pydis_site/apps/api/migrations/0070_new_filter_schema.py b/pydis_site/apps/api/migrations/0070_new_filter_schema.py
index f4fc9494..de75e677 100644
--- a/pydis_site/apps/api/migrations/0070_new_filter_schema.py
+++ b/pydis_site/apps/api/migrations/0070_new_filter_schema.py
@@ -143,7 +143,7 @@ class Migration(migrations.Migration):
('name', models.CharField(help_text='The unique name of this list.', max_length=50)),
('list_type', models.IntegerField(choices=[], help_text='Whenever this list is an allowlist or denylist')),
('default_settings', models.ForeignKey(help_text='Default parameters of this list.', on_delete=django.db.models.deletion.CASCADE, to='api.FilterSettings')),
- ('filters', models.ManyToManyField(help_text='The content of this list.', to='api.Filter')),
+ ('filters', models.ManyToManyField(help_text='The content of this list.', to='api.Filter', default=[])),
],
),
migrations.AddField(
diff --git a/pydis_site/apps/api/models/bot/filters.py b/pydis_site/apps/api/models/bot/filters.py
index 16ac206e..869f947c 100644
--- a/pydis_site/apps/api/models/bot/filters.py
+++ b/pydis_site/apps/api/models/bot/filters.py
@@ -49,7 +49,7 @@ class FilterList(models.Model):
help_text="Whenever this list is an allowlist or denylist"
)
- filters = models.ManyToManyField("Filter", help_text="The content of this list.")
+ filters = models.ManyToManyField("Filter", help_text="The content of this list.", default=[])
default_settings = models.ForeignKey(
"FilterSettings",
models.CASCADE,
diff --git a/pydis_site/apps/api/tests/test_filterlists.py b/pydis_site/apps/api/tests/test_filterlists.py
deleted file mode 100644
index 5a5bca60..00000000
--- a/pydis_site/apps/api/tests/test_filterlists.py
+++ /dev/null
@@ -1,122 +0,0 @@
-from django.urls import reverse
-
-from pydis_site.apps.api.models import FilterList
-from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase
-
-URL = reverse('api:bot:filterlist-list')
-JPEG_ALLOWLIST = {
- "type": 'FILE_FORMAT',
- "allowed": True,
- "content": ".jpeg",
-}
-PNG_ALLOWLIST = {
- "type": 'FILE_FORMAT',
- "allowed": True,
- "content": ".png",
-}
-
-
-class UnauthenticatedTests(AuthenticatedAPITestCase):
- def setUp(self):
- super().setUp()
- self.client.force_authenticate(user=None)
-
- def test_cannot_read_allowedlist_list(self):
- response = self.client.get(URL)
-
- self.assertEqual(response.status_code, 401)
-
-
-class EmptyDatabaseTests(AuthenticatedAPITestCase):
- @classmethod
- def setUpTestData(cls):
- FilterList.objects.all().delete()
-
- def test_returns_empty_object(self):
- response = self.client.get(URL)
-
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.json(), [])
-
-
-class FetchTests(AuthenticatedAPITestCase):
- @classmethod
- def setUpTestData(cls):
- FilterList.objects.all().delete()
- cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST)
- cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST)
-
- def test_returns_name_in_list(self):
- response = self.client.get(URL)
-
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.json()[0]["content"], self.jpeg_format.content)
- self.assertEqual(response.json()[1]["content"], self.png_format.content)
-
- def test_returns_single_item_by_id(self):
- response = self.client.get(f'{URL}/{self.jpeg_format.id}')
-
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.json().get("content"), self.jpeg_format.content)
-
- def test_returns_filter_list_types(self):
- response = self.client.get(f'{URL}/get-types')
-
- self.assertEqual(response.status_code, 200)
- for api_type, model_type in zip(response.json(), FilterList.FilterListType.choices):
- self.assertEquals(api_type[0], model_type[0])
- self.assertEquals(api_type[1], model_type[1])
-
-
-class CreationTests(AuthenticatedAPITestCase):
- @classmethod
- def setUpTestData(cls):
- FilterList.objects.all().delete()
-
- def test_returns_400_for_missing_params(self):
- no_type_json = {
- "allowed": True,
- "content": ".jpeg"
- }
- no_allowed_json = {
- "type": "FILE_FORMAT",
- "content": ".jpeg"
- }
- no_content_json = {
- "allowed": True,
- "type": "FILE_FORMAT"
- }
- cases = [{}, no_type_json, no_allowed_json, no_content_json]
-
- for case in cases:
- with self.subTest(case=case):
- response = self.client.post(URL, data=case)
- self.assertEqual(response.status_code, 400)
-
- def test_returns_201_for_successful_creation(self):
- response = self.client.post(URL, data=JPEG_ALLOWLIST)
- self.assertEqual(response.status_code, 201)
-
- def test_returns_400_for_duplicate_creation(self):
- self.client.post(URL, data=JPEG_ALLOWLIST)
- response = self.client.post(URL, data=JPEG_ALLOWLIST)
- self.assertEqual(response.status_code, 400)
-
-
-class DeletionTests(AuthenticatedAPITestCase):
- @classmethod
- def setUpTestData(cls):
- FilterList.objects.all().delete()
- cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST)
- cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST)
-
- def test_deleting_unknown_id_returns_404(self):
- response = self.client.delete(f"{URL}/200")
- self.assertEqual(response.status_code, 404)
-
- def test_deleting_known_id_returns_204(self):
- response = self.client.delete(f"{URL}/{self.jpeg_format.id}")
- self.assertEqual(response.status_code, 204)
-
- response = self.client.get(f"{URL}/{self.jpeg_format.id}")
- self.assertNotIn(self.png_format.content, response.json())
diff --git a/pydis_site/apps/api/tests/test_filters.py b/pydis_site/apps/api/tests/test_filters.py
new file mode 100644
index 00000000..de78ecfd
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_filters.py
@@ -0,0 +1,284 @@
+import contextlib
+from dataclasses import dataclass
+from typing import Any, Dict, Tuple, Type
+
+from django.db.models import Model
+from django_hosts 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
+
+
+@dataclass()
+class TestSequence:
+ model: Type[Model]
+ route: str
+ object: Dict[str, Any]
+ ignored_fields: Tuple[str] = ()
+
+ def url(self, detail: bool = False) -> str:
+ return reverse(f'bot:{self.route}-{"detail" if detail else "list"}', host='api')
+
+
+FK_FIELDS: Dict[Type[Model], Tuple[str]] = {
+ FilterList: ("default_settings",),
+ FilterSettings: ("default_action", "default_range"),
+ FilterAction: (),
+ ChannelRange: (),
+ Filter: (),
+ FilterOverride: ("filter_action", "filter_range")
+}
+
+
+def get_test_sequences() -> Dict[str, TestSequence]:
+ return {
+ "filter_list": TestSequence(
+ FilterList,
+ "filterlist",
+ {
+ "name": "testname",
+ "list_type": 0,
+ "default_settings": FilterSettings(
+ ping_type=[],
+ filter_dm=False,
+ dm_ping_type=[],
+ delete_messages=False,
+ bypass_roles=[],
+ enabled=False,
+ default_action=FilterAction(
+ user_dm=None,
+ infraction_type=None,
+ infraction_reason="",
+ infraction_duration=None
+ ),
+ default_range=ChannelRange(
+ disallowed_channels=[],
+ disallowed_categories=[],
+ allowed_channels=[],
+ allowed_category=[],
+ default=False
+ )
+ )
+ },
+ ignored_fields=("filters",)
+ ),
+ "filter_settings": TestSequence(
+ FilterSettings,
+ "filtersettings",
+ {
+ "ping_type": ["onduty"],
+ "filter_dm": True,
+ "dm_ping_type": ["123456"],
+ "delete_messages": True,
+ "bypass_roles": [123456],
+ "enabled": True,
+ "default_action": FilterAction(
+ user_dm=None,
+ infraction_type=None,
+ infraction_reason="",
+ infraction_duration=None
+ ),
+ "default_range": ChannelRange(
+ disallowed_channels=[],
+ disallowed_categories=[],
+ allowed_channels=[],
+ allowed_category=[],
+ default=False
+ )
+ }
+ ),
+ "filter_action": TestSequence(
+ FilterAction,
+ "filteraction",
+ {
+ "user_dm": "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_category": [1121],
+ "default": True
+ }
+ ),
+ "filter": TestSequence(
+ Filter,
+ "filter",
+ {
+ "content": "bad word",
+ "description": "This is a really bad word.",
+ "additional_field": None,
+ "override": None
+ }
+ ),
+ "filter_override": TestSequence(
+ FilterOverride,
+ "filteroverride",
+ {
+ "ping_type": ["everyone"],
+ "filter_dm": False,
+ "dm_ping_type": ["here"],
+ "delete_messages": 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__]:
+ value = getattr(object_, field)
+
+ if value is not None:
+ save_nested_objects(value)
+
+ if save_root:
+ object_.save()
+
+
+def clean_test_json(json: dict) -> dict:
+ for key, value in json.items():
+ if isinstance(value, Model):
+ json[key] = value.id
+
+ return json
+
+
+def clean_api_json(json: dict, sequence: TestSequence) -> dict:
+ for field in sequence.ignored_fields + ("id",):
+ with contextlib.suppress(KeyError):
+ del json[field]
+
+ return json
+
+
+class GenericFilterTest(APISubdomainTestCase):
+ def test_cannot_read_unauthenticated(self) -> None:
+ for name, sequence in get_test_sequences().items():
+ with self.subTest(name=name):
+ self.client.force_authenticate(user=None)
+
+ response = self.client.get(sequence.url())
+ self.assertEqual(response.status_code, 401)
+
+ def test_empty_database(self) -> None:
+ for name, sequence in get_test_sequences().items():
+ with self.subTest(name=name):
+ sequence.model.objects.all().delete()
+
+ response = self.client.get(sequence.url())
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [])
+
+ def test_fetch(self) -> None:
+ for name, sequence in get_test_sequences().items():
+ with self.subTest(name=name):
+ sequence.model.objects.all().delete()
+
+ save_nested_objects(sequence.model(**sequence.object))
+
+ response = self.client.get(sequence.url())
+ self.assertDictEqual(
+ clean_test_json(sequence.object),
+ clean_api_json(response.json()[0], sequence)
+ )
+
+ def test_fetch_by_id(self) -> None:
+ for name, sequence in get_test_sequences().items():
+ with self.subTest(name=name):
+ sequence.model.objects.all().delete()
+
+ saved = sequence.model(**sequence.object)
+ save_nested_objects(saved)
+
+ response = self.client.get(f"{sequence.url()}/{saved.id}")
+ self.assertDictEqual(
+ clean_test_json(sequence.object),
+ clean_api_json(response.json(), sequence)
+ )
+
+ def test_fetch_non_existing(self) -> None:
+ for name, sequence in get_test_sequences().items():
+ with self.subTest(name=name):
+ sequence.model.objects.all().delete()
+
+ response = self.client.get(f"{sequence.url()}/42")
+ self.assertEqual(response.status_code, 404)
+ self.assertDictEqual(response.json(), {'detail': 'Not found.'})
+
+ def test_creation(self) -> None:
+ for name, sequence in get_test_sequences().items():
+ with self.subTest(name=name):
+ sequence.model.objects.all().delete()
+
+ save_nested_objects(sequence.model(**sequence.object), False)
+ data = clean_test_json(sequence.object.copy())
+ response = self.client.post(sequence.url(), data=data)
+
+ self.assertEqual(response.status_code, 201)
+ self.assertDictEqual(
+ clean_api_json(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)
+ data = clean_test_json(sequence.object.copy())
+
+ for field in sequence.model._meta.get_fields():
+ with self.subTest(field=field):
+ if field.null or field.name in sequence.ignored_fields + ("id",):
+ continue
+
+ test_data = data.copy()
+ del test_data[field.name]
+
+ response = self.client.post(sequence.url(), data=test_data)
+ self.assertEqual(response.status_code, 400)
+
+ def test_deletion(self) -> None:
+ for name, sequence in get_test_sequences().items():
+ with self.subTest(name=name):
+ saved = sequence.model(**sequence.object)
+ save_nested_objects(saved)
+
+ response = self.client.delete(f"{sequence.url()}/{saved.id}")
+ self.assertEqual(response.status_code, 204)
+
+ def test_deletion_non_existing(self) -> None:
+ for name, sequence in get_test_sequences().items():
+ with self.subTest(name=name):
+ sequence.model.objects.all().delete()
+
+ response = self.client.delete(f"{sequence.url()}/42")
+ self.assertEqual(response.status_code, 404)
+
+ def test_reject_invalid_ping(self) -> None:
+ url = reverse('bot:filteroverride-list', host='api')
+ data = {
+ "ping_type": ["invalid"]
+ }
+
+ response = self.client.post(url, data=data)
+
+ self.assertEqual(response.status_code, 400)
+ self.assertDictEqual(response.json(), {'ping_type': ["'invalid' isn't a valid ping type."]})
diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py
index 5c9ddea4..c8f4e1b1 100644
--- a/pydis_site/apps/api/tests/test_models.py
+++ b/pydis_site/apps/api/tests/test_models.py
@@ -7,6 +7,9 @@ from django.utils import timezone
from pydis_site.apps.api.models import (
DeletedMessage,
DocumentationLink,
+ Filter,
+ FilterList,
+ FilterSettings,
Infraction,
Message,
MessageDeletionContext,
@@ -106,6 +109,17 @@ class StringDunderMethodTests(SimpleTestCase):
DocumentationLink(
'test', 'http://example.com', 'http://example.com'
),
+ 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,
channel_id=291284109232308226,