aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps/api/tests
diff options
context:
space:
mode:
authorGravatar Gareth Coles <[email protected]>2019-04-05 18:24:32 +0100
committerGravatar Gareth Coles <[email protected]>2019-04-05 18:24:32 +0100
commitab8b798547e82ca79882ba28b1920077c803425f (patch)
treeb28a9005d05dfd2c9da62351671bf2aa6e37f7dc /pydis_site/apps/api/tests
parent[#158 #160] Automatically run collectstatic in containers/setup script (diff)
pysite -> pydis_site
Diffstat (limited to 'pydis_site/apps/api/tests')
-rw-r--r--pydis_site/apps/api/tests/__init__.py0
-rw-r--r--pydis_site/apps/api/tests/base.py69
-rw-r--r--pydis_site/apps/api/tests/test_deleted_messages.py43
-rw-r--r--pydis_site/apps/api/tests/test_documentation_links.py161
-rw-r--r--pydis_site/apps/api/tests/test_healthcheck.py16
-rw-r--r--pydis_site/apps/api/tests/test_infractions.py359
-rw-r--r--pydis_site/apps/api/tests/test_models.py113
-rw-r--r--pydis_site/apps/api/tests/test_nominations.py41
-rw-r--r--pydis_site/apps/api/tests/test_off_topic_channel_names.py152
-rw-r--r--pydis_site/apps/api/tests/test_rules.py35
-rw-r--r--pydis_site/apps/api/tests/test_snake_names.py67
-rw-r--r--pydis_site/apps/api/tests/test_users.py121
-rw-r--r--pydis_site/apps/api/tests/test_validators.py213
13 files changed, 1390 insertions, 0 deletions
diff --git a/pydis_site/apps/api/tests/__init__.py b/pydis_site/apps/api/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/pydis_site/apps/api/tests/__init__.py
diff --git a/pydis_site/apps/api/tests/base.py b/pydis_site/apps/api/tests/base.py
new file mode 100644
index 00000000..0290fa69
--- /dev/null
+++ b/pydis_site/apps/api/tests/base.py
@@ -0,0 +1,69 @@
+from django.contrib.auth.models import User
+from rest_framework.test import APITestCase
+
+
+test_user, _created = User.objects.get_or_create(
+ username='test',
+ password='testpass', # noqa: S106
+ is_superuser=True,
+ is_staff=True
+)
+
+
+class APISubdomainTestCase(APITestCase):
+ """
+ Configures the test client to use the proper subdomain
+ for requests and forces authentication for the test user.
+
+ The test user is considered staff and superuser.
+ If you want to test for a custom user (for example, to test model permissions),
+ create the user, assign the relevant permissions, and use
+ `self.client.force_authenticate(user=created_user)` to force authentication
+ through the created user.
+
+ Using this performs the following niceties for you which ease writing tests:
+ - setting the `HTTP_HOST` request header to `api.pythondiscord.local:8000`, and
+ - forcing authentication for the test user.
+ If you don't want to force authentication (for example, to test a route's response
+ for an unauthenticated user), un-force authentication by using the following:
+
+ >>> from pydis_site.apps.api import APISubdomainTestCase
+ >>> class UnauthedUserTestCase(APISubdomainTestCase):
+ ... def setUp(self):
+ ... super().setUp()
+ ... self.client.force_authentication(user=None)
+ ... def test_can_read_objects_at_my_endpoint(self):
+ ... resp = self.client.get('/my-publicly-readable-endpoint')
+ ... self.assertEqual(resp.status_code, 200)
+ ... def test_cannot_delete_objects_at_my_endpoint(self):
+ ... resp = self.client.delete('/my-publicly-readable-endpoint/42')
+ ... self.assertEqual(resp.status_code, 401)
+
+ Make sure to include the `super().setUp(self)` call, otherwise, you may get
+ status code 404 for some URLs due to the missing `HTTP_HOST` header.
+
+ ## Example
+ Using this in a test case is rather straightforward:
+
+ >>> from pydis_site.apps.api import APISubdomainTestCase
+ >>> class MyAPITestCase(APISubdomainTestCase):
+ ... def test_that_it_works(self):
+ ... response = self.client.get('/my-endpoint')
+ ... self.assertEqual(response.status_code, 200)
+
+ To reverse URLs of the API host, you need to use `django_hosts`:
+
+ >>> from django_hosts.resolvers import reverse
+ >>> from pydis_site.apps.api import APISubdomainTestCase
+ >>> class MyReversedTestCase(APISubdomainTestCase):
+ ... def test_my_endpoint(self):
+ ... url = reverse('user-detail', host='api')
+ ... response = self.client.get(url)
+ ... self.assertEqual(response.status_code, 200)
+ """
+
+ def setUp(self):
+ super().setUp()
+ self.client.defaults['HTTP_HOST'] = 'api.pythondiscord.local:8000'
+ self.client.force_authenticate(test_user)
diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py
new file mode 100644
index 00000000..cd5acab0
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_deleted_messages.py
@@ -0,0 +1,43 @@
+from datetime import datetime
+
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+from ..models import User
+
+
+class DeletedMessagesTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.author = User.objects.create(
+ id=55,
+ name='Robbie Rotten',
+ discriminator=55,
+ avatar_hash=None
+ )
+
+ cls.data = {
+ 'actor': None,
+ 'creation': datetime.utcnow().isoformat(),
+ 'deletedmessage_set': [
+ {
+ 'author': cls.author.id,
+ 'id': 55,
+ 'channel_id': 5555,
+ 'content': "Terror Billy is a meanie",
+ 'embeds': []
+ },
+ {
+ 'author': cls.author.id,
+ 'id': 56,
+ 'channel_id': 5555,
+ 'content': "If you purge this, you're evil",
+ 'embeds': []
+ }
+ ]
+ }
+
+ def test_accepts_valid_data(self):
+ url = reverse('bot:messagedeletioncontext-list', host='api')
+ response = self.client.post(url, data=self.data)
+ self.assertEqual(response.status_code, 201)
diff --git a/pydis_site/apps/api/tests/test_documentation_links.py b/pydis_site/apps/api/tests/test_documentation_links.py
new file mode 100644
index 00000000..f6c78391
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_documentation_links.py
@@ -0,0 +1,161 @@
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+from ..models import DocumentationLink
+
+
+class UnauthedDocumentationLinkAPITests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+ self.client.force_authenticate(user=None)
+
+ def test_detail_lookup_returns_401(self):
+ url = reverse('bot:documentationlink-detail', args=('whatever',), host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_list_returns_401(self):
+ url = reverse('bot:documentationlink-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_create_returns_401(self):
+ url = reverse('bot:documentationlink-list', host='api')
+ response = self.client.post(url, data={'hi': 'there'})
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_delete_returns_401(self):
+ url = reverse('bot:documentationlink-detail', args=('whatever',), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 401)
+
+
+class EmptyDatabaseDocumentationLinkAPITests(APISubdomainTestCase):
+ def test_detail_lookup_returns_404(self):
+ url = reverse('bot:documentationlink-detail', args=('whatever',), host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_list_all_returns_empty_list(self):
+ url = reverse('bot:documentationlink-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [])
+
+ def test_delete_returns_404(self):
+ url = reverse('bot:documentationlink-detail', args=('whatever',), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 404)
+
+
+class DetailLookupDocumentationLinkAPITests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.doc_link = DocumentationLink.objects.create(
+ package='testpackage',
+ base_url='https://example.com',
+ inventory_url='https://example.com'
+ )
+
+ cls.doc_json = {
+ 'package': cls.doc_link.package,
+ 'base_url': cls.doc_link.base_url,
+ 'inventory_url': cls.doc_link.inventory_url
+ }
+
+ def test_detail_lookup_unknown_package_returns_404(self):
+ url = reverse('bot:documentationlink-detail', args=('whatever',), host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_detail_lookup_created_package_returns_package(self):
+ url = reverse('bot:documentationlink-detail', args=(self.doc_link.package,), host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), self.doc_json)
+
+ def test_list_all_packages_shows_created_package(self):
+ url = reverse('bot:documentationlink-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [self.doc_json])
+
+ def test_create_invalid_body_returns_400(self):
+ url = reverse('bot:documentationlink-list', host='api')
+ response = self.client.post(url, data={'i': 'am', 'totally': 'valid'})
+
+ self.assertEqual(response.status_code, 400)
+
+ def test_create_invalid_url_returns_400(self):
+ body = {
+ 'package': 'example',
+ 'base_url': 'https://example.com',
+ 'inventory_url': 'totally an url'
+ }
+
+ url = reverse('bot:documentationlink-list', host='api')
+ response = self.client.post(url, data=body)
+
+ self.assertEqual(response.status_code, 400)
+
+
+class DocumentationLinkCreationTests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.body = {
+ 'package': 'example',
+ 'base_url': 'https://example.com',
+ 'inventory_url': 'https://docs.example.com'
+ }
+
+ url = reverse('bot:documentationlink-list', host='api')
+ response = self.client.post(url, data=self.body)
+
+ self.assertEqual(response.status_code, 201)
+
+ def test_package_in_full_list(self):
+ url = reverse('bot:documentationlink-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [self.body])
+
+ def test_detail_lookup_works_with_package(self):
+ url = reverse('bot:documentationlink-detail', args=(self.body['package'],), host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), self.body)
+
+
+class DocumentationLinkDeletionTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.doc_link = DocumentationLink.objects.create(
+ package='example',
+ base_url='https://example.com',
+ inventory_url='https://docs.example.com'
+ )
+
+ def test_unknown_package_returns_404(self):
+ url = reverse('bot:documentationlink-detail', args=('whatever',), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_delete_known_package_returns_204(self):
+ url = reverse('bot:documentationlink-detail', args=(self.doc_link.package,), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 204)
diff --git a/pydis_site/apps/api/tests/test_healthcheck.py b/pydis_site/apps/api/tests/test_healthcheck.py
new file mode 100644
index 00000000..b0fd71bf
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_healthcheck.py
@@ -0,0 +1,16 @@
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+
+
+class UnauthedHealthcheckAPITests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+ self.client.force_authenticate(user=None)
+
+ def test_can_access_healthcheck_view(self):
+ url = reverse('healthcheck', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), {'status': 'ok'})
diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py
new file mode 100644
index 00000000..7c370c17
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_infractions.py
@@ -0,0 +1,359 @@
+from datetime import datetime as dt, timedelta, timezone
+from urllib.parse import quote
+
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+from ..models import Infraction, User
+
+
+class UnauthenticatedTests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+ self.client.force_authenticate(user=None)
+
+ def test_detail_lookup_returns_401(self):
+ url = reverse('bot:infraction-detail', args=(5,), host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_list_returns_401(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_create_returns_401(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.post(url, data={'reason': 'Have a nice day.'})
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_partial_update_returns_401(self):
+ url = reverse('bot:infraction-detail', args=(5,), host='api')
+ response = self.client.patch(url, data={'reason': 'Have a nice day.'})
+
+ self.assertEqual(response.status_code, 401)
+
+
+class InfractionTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.user = User.objects.create(
+ id=5,
+ name='james',
+ discriminator=1,
+ avatar_hash=None
+ )
+ cls.ban_hidden = Infraction.objects.create(
+ user_id=cls.user.id,
+ actor_id=cls.user.id,
+ type='ban',
+ reason='He terk my jerb!',
+ hidden=True,
+ expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc)
+ )
+ cls.ban_inactive = Infraction.objects.create(
+ user_id=cls.user.id,
+ actor_id=cls.user.id,
+ type='ban',
+ reason='James is an ass, and we won\'t be working with him again.',
+ active=False
+ )
+
+ def test_list_all(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ infractions = response.json()
+
+ self.assertEqual(len(infractions), 2)
+ self.assertEqual(infractions[0]['id'], self.ban_hidden.id)
+ self.assertEqual(infractions[1]['id'], self.ban_inactive.id)
+
+ def test_filter_search(self):
+ url = reverse('bot:infraction-list', host='api')
+ pattern = quote(r'^James(\s\w+){3},')
+ response = self.client.get(f'{url}?search={pattern}')
+
+ self.assertEqual(response.status_code, 200)
+ infractions = response.json()
+
+ self.assertEqual(len(infractions), 1)
+ self.assertEqual(infractions[0]['id'], self.ban_inactive.id)
+
+ def test_filter_field(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(f'{url}?type=ban&hidden=true')
+
+ self.assertEqual(response.status_code, 200)
+ infractions = response.json()
+
+ self.assertEqual(len(infractions), 1)
+ self.assertEqual(infractions[0]['id'], self.ban_hidden.id)
+
+ def test_returns_empty_for_no_match(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(f'{url}?type=ban&search=poop')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.json()), 0)
+
+ def test_ignores_bad_filters(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(f'{url}?type=ban&hidden=maybe&foo=bar')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.json()), 2)
+
+ def test_retrieve_single_from_id(self):
+ url = reverse('bot:infraction-detail', args=(self.ban_inactive.id,), host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json()['id'], self.ban_inactive.id)
+
+ def test_retrieve_returns_404_for_absent_id(self):
+ url = reverse('bot:infraction-detail', args=(1337,), host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_partial_update(self):
+ url = reverse('bot:infraction-detail', args=(self.ban_hidden.id,), host='api')
+ data = {
+ 'expires_at': '4143-02-15T21:04:31+00:00',
+ 'active': False,
+ 'reason': 'durka derr'
+ }
+
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 200)
+ infraction = Infraction.objects.get(id=self.ban_hidden.id)
+
+ # These fields were updated.
+ self.assertEqual(infraction.expires_at.isoformat(), data['expires_at'])
+ self.assertEqual(infraction.active, data['active'])
+ self.assertEqual(infraction.reason, data['reason'])
+
+ # These fields are still the same.
+ self.assertEqual(infraction.id, self.ban_hidden.id)
+ self.assertEqual(infraction.inserted_at, self.ban_hidden.inserted_at)
+ self.assertEqual(infraction.user.id, self.ban_hidden.user.id)
+ self.assertEqual(infraction.actor.id, self.ban_hidden.actor.id)
+ self.assertEqual(infraction.type, self.ban_hidden.type)
+ self.assertEqual(infraction.hidden, self.ban_hidden.hidden)
+
+ def test_partial_update_returns_400_for_frozen_field(self):
+ url = reverse('bot:infraction-detail', args=(self.ban_hidden.id,), host='api')
+ data = {'user': 6}
+
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'user': ['This field cannot be updated.']
+ })
+
+
+class CreationTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.user = User.objects.create(
+ id=5,
+ name='james',
+ discriminator=1,
+ avatar_hash=None
+ )
+
+ def test_accepts_valid_data(self):
+ url = reverse('bot:infraction-list', host='api')
+ data = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'ban',
+ 'reason': 'He terk my jerb!',
+ 'hidden': True,
+ 'expires_at': '5018-11-20T15:52:00+00:00'
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 201)
+
+ infraction = Infraction.objects.get(id=response.json()['id'])
+ self.assertAlmostEqual(
+ infraction.inserted_at,
+ dt.now(timezone.utc),
+ delta=timedelta(seconds=2)
+ )
+ self.assertEqual(infraction.expires_at.isoformat(), data['expires_at'])
+ self.assertEqual(infraction.user.id, data['user'])
+ self.assertEqual(infraction.actor.id, data['actor'])
+ self.assertEqual(infraction.type, data['type'])
+ self.assertEqual(infraction.reason, data['reason'])
+ self.assertEqual(infraction.hidden, data['hidden'])
+ self.assertEqual(infraction.active, True)
+
+ def test_returns_400_for_missing_user(self):
+ url = reverse('bot:infraction-list', host='api')
+ data = {
+ 'actor': self.user.id,
+ 'type': 'kick'
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'user': ['This field is required.']
+ })
+
+ def test_returns_400_for_bad_user(self):
+ url = reverse('bot:infraction-list', host='api')
+ data = {
+ 'user': 1337,
+ 'actor': self.user.id,
+ 'type': 'kick'
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'user': ['Invalid pk "1337" - object does not exist.']
+ })
+
+ def test_returns_400_for_bad_type(self):
+ url = reverse('bot:infraction-list', host='api')
+ data = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'hug'
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'type': ['"hug" is not a valid choice.']
+ })
+
+ def test_returns_400_for_bad_expired_at_format(self):
+ url = reverse('bot:infraction-list', host='api')
+ data = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'ban',
+ 'expires_at': '20/11/5018 15:52:00'
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'expires_at': [
+ 'Datetime has wrong format. Use one of these formats instead: '
+ 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'
+ ]
+ })
+
+ def test_returns_400_for_expiring_non_expirable_type(self):
+ url = reverse('bot:infraction-list', host='api')
+ data = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'kick',
+ 'expires_at': '5018-11-20T15:52:00+00:00'
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'expires_at': [f'{data["type"]} infractions cannot expire.']
+ })
+
+ def test_returns_400_for_hidden_non_hideable_type(self):
+ url = reverse('bot:infraction-list', host='api')
+ data = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'superstar',
+ 'hidden': True
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'hidden': [f'{data["type"]} infractions cannot be hidden.']
+ })
+
+
+class ExpandedTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.user = User.objects.create(
+ id=5,
+ name='james',
+ discriminator=1,
+ avatar_hash=None
+ )
+ cls.kick = Infraction.objects.create(
+ user_id=cls.user.id,
+ actor_id=cls.user.id,
+ type='kick'
+ )
+ cls.warning = Infraction.objects.create(
+ user_id=cls.user.id,
+ actor_id=cls.user.id,
+ type='warning'
+ )
+
+ def check_expanded_fields(self, infraction):
+ for key in ('user', 'actor'):
+ obj = infraction[key]
+ for field in ('id', 'name', 'discriminator', 'avatar_hash', 'roles', 'in_guild'):
+ self.assertTrue(field in obj, msg=f'field "{field}" missing from {key}')
+
+ def test_list_expanded(self):
+ url = reverse('bot:infraction-list-expanded', host='api')
+
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ response_data = response.json()
+ self.assertEqual(len(response_data), 2)
+
+ for infraction in response_data:
+ self.check_expanded_fields(infraction)
+
+ def test_create_expanded(self):
+ url = reverse('bot:infraction-list-expanded', host='api')
+ data = {
+ 'user': self.user.id,
+ 'actor': self.user.id,
+ 'type': 'warning'
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 201)
+
+ self.assertEqual(len(Infraction.objects.all()), 3)
+ self.check_expanded_fields(response.json())
+
+ def test_retrieve_expanded(self):
+ url = reverse('bot:infraction-detail-expanded', args=(self.warning.id,), host='api')
+
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ infraction = response.json()
+ self.assertEqual(infraction['id'], self.warning.id)
+ self.check_expanded_fields(infraction)
+
+ def test_partial_update_expanded(self):
+ url = reverse('bot:infraction-detail-expanded', args=(self.kick.id,), host='api')
+ data = {'active': False}
+
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 200)
+
+ infraction = Infraction.objects.get(id=self.kick.id)
+ self.assertEqual(infraction.active, data['active'])
+ self.check_expanded_fields(response.json())
diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py
new file mode 100644
index 00000000..43d1eb41
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_models.py
@@ -0,0 +1,113 @@
+from datetime import datetime as dt, timezone
+
+from django.test import SimpleTestCase
+
+from ..models import (
+ BotSetting, DeletedMessage,
+ DocumentationLink, Infraction,
+ Message, MessageDeletionContext,
+ ModelReprMixin, OffTopicChannelName,
+ Reminder, Role,
+ SnakeFact, SnakeIdiom,
+ SnakeName, SpecialSnake,
+ Tag, User
+)
+
+
+class SimpleClass(ModelReprMixin):
+ def __init__(self, is_what):
+ self.the_cake = is_what
+
+
+class ReprMixinTests(SimpleTestCase):
+ def setUp(self):
+ self.klass = SimpleClass('is a lie')
+
+ def test_shows_attributes(self):
+ expected = "<SimpleClass(the_cake='is a lie')>"
+ self.assertEqual(repr(self.klass), expected)
+
+
+class StringDunderMethodTests(SimpleTestCase):
+ def setUp(self):
+ self.objects = (
+ DeletedMessage(
+ id=45,
+ author=User(
+ id=444, name='bill',
+ discriminator=5, avatar_hash=None
+ ),
+ channel_id=666,
+ content="wooey",
+ deletion_context=MessageDeletionContext(
+ actor=User(
+ id=5555, name='shawn',
+ discriminator=555, avatar_hash=None
+ ),
+ creation=dt.utcnow()
+ ),
+ embeds=[]
+ ),
+ DocumentationLink(
+ 'test', 'http://example.com', 'http://example.com'
+ ),
+ OffTopicChannelName(name='bob-the-builders-playground'),
+ SnakeFact(fact='snakes are cute'),
+ SnakeIdiom(idiom='snake snacks'),
+ SnakeName(name='python', scientific='3'),
+ SpecialSnake(
+ name='Pythagoras Pythonista',
+ info='The only python snake that is born a triangle'
+ ),
+ Role(
+ id=5, name='test role',
+ colour=0x5, permissions=0
+ ),
+ Message(
+ id=45,
+ author=User(
+ id=444, name='bill',
+ discriminator=5, avatar_hash=None
+ ),
+ channel_id=666,
+ content="wooey",
+ embeds=[]
+ ),
+ MessageDeletionContext(
+ actor=User(
+ id=5555, name='shawn',
+ discriminator=555, avatar_hash=None
+ ),
+ creation=dt.utcnow()
+ ),
+ Tag(
+ title='bob',
+ embed={'content': "the builder"}
+ ),
+ User(
+ id=5, name='bob',
+ discriminator=1, avatar_hash=None
+ ),
+ Infraction(
+ user_id=5, actor_id=5,
+ type='kick', reason='He terk my jerb!'
+ ),
+ Infraction(
+ user_id=5, actor_id=5, hidden=True,
+ type='kick', reason='He terk my jerb!',
+ expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc)
+ ),
+ Reminder(
+ author=User(
+ id=452, name='billy',
+ discriminator=5, avatar_hash=None
+ ),
+ channel_id=555,
+ content="oh no",
+ expiration=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc)
+ )
+ )
+
+ def test_returns_string(self):
+ for instance in self.objects:
+ self.assertIsInstance(str(instance), str)
diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py
new file mode 100644
index 00000000..1f03d1b0
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_nominations.py
@@ -0,0 +1,41 @@
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+from ..models import Nomination, User
+
+
+class NominationTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.author = User.objects.create(
+ id=5152,
+ name='Ro Bert',
+ discriminator=256,
+ avatar_hash=None
+ )
+ cls.user = cls.author
+
+ cls.nomination = Nomination.objects.create(
+ author=cls.author,
+ reason="he's good",
+ user=cls.author
+ )
+
+ def test_returns_400_on_attempt_to_update_frozen_field(self):
+ url = reverse('bot:nomination-detail', args=(self.user.id,), host='api')
+ response = self.client.put(
+ url,
+ data={'inserted_at': 'something bad'}
+ )
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'inserted_at': ['This field cannot be updated.']
+ })
+
+ def test_returns_200_on_successful_update(self):
+ url = reverse('bot:nomination-detail', args=(self.user.id,), host='api')
+ response = self.client.patch(
+ url,
+ data={'reason': 'there are many like it, but this test is mine'}
+ )
+ self.assertEqual(response.status_code, 200)
diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py
new file mode 100644
index 00000000..60af1f62
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py
@@ -0,0 +1,152 @@
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+from ..models import OffTopicChannelName
+
+
+class UnauthenticatedTests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+ self.client.force_authenticate(user=None)
+
+ def test_cannot_read_off_topic_channel_name_list(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(f'{url}?random_items=no')
+
+ self.assertEqual(response.status_code, 401)
+
+
+class EmptyDatabaseTests(APISubdomainTestCase):
+ def test_returns_empty_object(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [])
+
+ def test_returns_empty_list_with_get_all_param(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(f'{url}?random_items=5')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [])
+
+ def test_returns_400_for_bad_random_items_param(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(f'{url}?random_items=totally-a-valid-integer')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'random_items': ["Must be a valid integer."]
+ })
+
+ def test_returns_400_for_negative_random_items_param(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(f'{url}?random_items=-5')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'random_items': ["Must be a positive integer."]
+ })
+
+
+class ListTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand')
+ cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk')
+
+ def test_returns_name_in_list(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.json(),
+ [
+ self.test_name.name,
+ self.test_name_2.name
+ ]
+ )
+
+ def test_returns_single_item_with_random_items_param_set_to_1(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(f'{url}?random_items=1')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.json()), 1)
+
+
+class CreationTests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ self.name = "lemonade-shop"
+ response = self.client.post(f'{url}?name={self.name}')
+ self.assertEqual(response.status_code, 201)
+
+ def test_name_in_full_list(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [self.name])
+
+ def test_returns_400_for_missing_name_param(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.post(url)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'name': ["This query parameter is required."]
+ })
+
+ def test_returns_400_for_bad_name_param(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ invalid_names = (
+ 'space between words',
+ 'UPPERCASE',
+ '$$$$$$$$'
+ )
+
+ for name in invalid_names:
+ response = self.client.post(f'{url}?name={name}')
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'name': ["Enter a valid value."]
+ })
+
+
+class DeletionTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand')
+ cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk')
+
+ def test_deleting_unknown_name_returns_404(self):
+ url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_deleting_known_name_returns_204(self):
+ url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 204)
+
+ def test_name_gets_deleted(self):
+ url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 204)
+
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(url)
+ self.assertNotIn(self.test_name_2.name, response.json())
diff --git a/pydis_site/apps/api/tests/test_rules.py b/pydis_site/apps/api/tests/test_rules.py
new file mode 100644
index 00000000..c94f89cc
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_rules.py
@@ -0,0 +1,35 @@
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+from ..views import RulesView
+
+
+class RuleAPITests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+ self.client.force_authenticate(user=None)
+
+ def test_can_access_rules_view(self):
+ url = reverse('rules', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIsInstance(response.json(), list)
+
+ def test_link_format_query_param_produces_different_results(self):
+ url = reverse('rules', host='api')
+ markdown_links_response = self.client.get(url + '?link_format=md')
+ html_links_response = self.client.get(url + '?link_format=html')
+ self.assertNotEqual(
+ markdown_links_response.json(),
+ html_links_response.json()
+ )
+
+ def test_format_link_raises_value_error_for_invalid_target(self):
+ with self.assertRaises(ValueError):
+ RulesView._format_link("a", "b", "c")
+
+ def test_get_returns_400_for_wrong_link_format(self):
+ url = reverse('rules', host='api')
+ response = self.client.get(url + '?link_format=unknown')
+ self.assertEqual(response.status_code, 400)
diff --git a/pydis_site/apps/api/tests/test_snake_names.py b/pydis_site/apps/api/tests/test_snake_names.py
new file mode 100644
index 00000000..41dfae63
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_snake_names.py
@@ -0,0 +1,67 @@
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+from ..models import SnakeName
+
+
+class StatusTests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+ self.client.force_authenticate(user=None)
+
+ def test_cannot_read_snake_name_list(self):
+ url = reverse('bot:snakename-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_cannot_read_snake_names_with_get_all_param(self):
+ url = reverse('bot:snakename-list', host='api')
+ response = self.client.get(f'{url}?get_all=True')
+
+ self.assertEqual(response.status_code, 401)
+
+
+class EmptyDatabaseSnakeNameTests(APISubdomainTestCase):
+ def test_endpoint_returns_empty_object(self):
+ url = reverse('bot:snakename-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), {})
+
+ def test_endpoint_returns_empty_list_with_get_all_param(self):
+ url = reverse('bot:snakename-list', host='api')
+ response = self.client.get(f'{url}?get_all=True')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [])
+
+
+class SnakeNameListTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.snake_python = SnakeName.objects.create(name='Python', scientific='Totally.')
+
+ def test_endpoint_returns_all_snakes_with_get_all_param(self):
+ url = reverse('bot:snakename-list', host='api')
+ response = self.client.get(f'{url}?get_all=True')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.json(),
+ [
+ {
+ 'name': self.snake_python.name,
+ 'scientific': self.snake_python.scientific
+ }
+ ]
+ )
+
+ def test_endpoint_returns_single_snake_without_get_all_param(self):
+ url = reverse('bot:snakename-list', host='api')
+ response = self.client.get(url)
+ self.assertEqual(response.json(), {
+ 'name': self.snake_python.name,
+ 'scientific': self.snake_python.scientific
+ })
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
new file mode 100644
index 00000000..90bc3d30
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -0,0 +1,121 @@
+from django_hosts.resolvers import reverse
+
+from .base import APISubdomainTestCase
+from ..models import Role, User
+
+
+class UnauthedUserAPITests(APISubdomainTestCase):
+ def setUp(self):
+ super().setUp()
+ self.client.force_authenticate(user=None)
+
+ def test_detail_lookup_returns_401(self):
+ url = reverse('bot:user-detail', args=('whatever',), host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_list_returns_401(self):
+ url = reverse('bot:user-list', host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_create_returns_401(self):
+ url = reverse('bot:user-list', host='api')
+ response = self.client.post(url, data={'hi': 'there'})
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_delete_returns_401(self):
+ url = reverse('bot:user-detail', args=('whatever',), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 401)
+
+
+class CreationTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls): # noqa
+ cls.role = Role.objects.create(
+ id=5,
+ name="Test role pls ignore",
+ colour=2,
+ permissions=0b01010010101
+ )
+
+ def test_accepts_valid_data(self):
+ url = reverse('bot:user-list', host='api')
+ data = {
+ 'id': 42,
+ 'avatar_hash': "validavatarhashiswear",
+ 'name': "Test",
+ 'discriminator': 42,
+ 'roles': [
+ self.role.id
+ ],
+ 'in_guild': True
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 201)
+ self.assertEqual(response.json(), data)
+
+ user = User.objects.get(id=42)
+ self.assertEqual(user.avatar_hash, data['avatar_hash'])
+ self.assertEqual(user.name, data['name'])
+ self.assertEqual(user.discriminator, data['discriminator'])
+ self.assertEqual(user.in_guild, data['in_guild'])
+
+ def test_supports_multi_creation(self):
+ url = reverse('bot:user-list', host='api')
+ data = [
+ {
+ 'id': 5,
+ 'avatar_hash': "hahayes",
+ 'name': "test man",
+ 'discriminator': 42,
+ 'roles': [
+ self.role.id
+ ],
+ 'in_guild': True
+ },
+ {
+ 'id': 8,
+ 'avatar_hash': "maybenot",
+ 'name': "another test man",
+ 'discriminator': 555,
+ 'roles': [],
+ 'in_guild': False
+ }
+ ]
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 201)
+ self.assertEqual(response.json(), data)
+
+ def test_returns_400_for_unknown_role_id(self):
+ url = reverse('bot:user-list', host='api')
+ data = {
+ 'id': 5,
+ 'avatar_hash': "hahayes",
+ 'name': "test man",
+ 'discriminator': 42,
+ 'roles': [
+ 190810291
+ ]
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
+
+ def test_returns_400_for_bad_data(self):
+ url = reverse('bot:user-list', host='api')
+ data = {
+ 'id': True,
+ 'avatar_hash': 1902831,
+ 'discriminator': "totally!"
+ }
+
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py
new file mode 100644
index 00000000..d2c0a136
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_validators.py
@@ -0,0 +1,213 @@
+from django.core.exceptions import ValidationError
+from django.test import TestCase
+
+from ..validators import (
+ validate_bot_setting_name,
+ validate_tag_embed
+)
+
+
+REQUIRED_KEYS = (
+ 'content', 'fields', 'image', 'title', 'video'
+)
+
+
+class BotSettingValidatorTests(TestCase):
+ def test_accepts_valid_names(self):
+ validate_bot_setting_name('defcon')
+
+ def test_rejects_bad_names(self):
+ with self.assertRaises(ValidationError):
+ validate_bot_setting_name('bad name')
+
+class TagEmbedValidatorTests(TestCase):
+ def test_rejects_non_mapping(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed('non-empty non-mapping')
+
+ def test_rejects_missing_required_keys(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'unknown': "key"
+ })
+
+ def test_rejects_one_correct_one_incorrect(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'provider': "??",
+ 'title': ""
+ })
+
+ def test_rejects_empty_required_key(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'title': ''
+ })
+
+ def test_rejects_list_as_embed(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed([])
+
+ def test_rejects_required_keys_and_unknown_keys(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'title': "the duck walked up to the lemonade stand",
+ 'and': "he said to the man running the stand"
+ })
+
+ def test_rejects_too_long_title(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'title': 'a' * 257
+ })
+
+ def test_rejects_too_many_fields(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'fields': [{} for _ in range(26)]
+ })
+
+ def test_rejects_too_long_description(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'description': 'd' * 2049
+ })
+
+ def test_allows_valid_embed(self):
+ validate_tag_embed({
+ 'title': "My embed",
+ 'description': "look at my embed, my embed is amazing"
+ })
+
+ def test_allows_unvalidated_fields(self):
+ validate_tag_embed({
+ 'title': "My embed",
+ 'provider': "what am I??"
+ })
+
+ def test_rejects_fields_as_list_of_non_mappings(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'fields': ['abc']
+ })
+
+ def test_rejects_fields_with_unknown_fields(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'fields': [
+ {
+ 'what': "is this field"
+ }
+ ]
+ })
+
+ def test_rejects_fields_with_too_long_name(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'fields': [
+ {
+ 'name': "a" * 257
+ }
+ ]
+ })
+
+ def test_rejects_one_correct_one_incorrect_field(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'fields': [
+ {
+ 'name': "Totally valid",
+ 'value': "LOOK AT ME"
+ },
+ {
+ 'oh': "what is this key?"
+ }
+ ]
+ })
+
+ def test_allows_valid_fields(self):
+ validate_tag_embed({
+ 'fields': [
+ {
+ 'name': "valid",
+ 'value': "field"
+ }
+ ]
+ })
+
+ def test_rejects_footer_as_non_mapping(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'title': "whatever",
+ 'footer': []
+ })
+
+ def test_rejects_footer_with_unknown_fields(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'title': "whatever",
+ 'footer': {
+ 'duck': "quack"
+ }
+ })
+
+ def test_rejects_footer_with_empty_text(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'title': "whatever",
+ 'footer': {
+ 'text': ""
+ }
+ })
+
+ def test_allows_footer_with_proper_values(self):
+ validate_tag_embed({
+ 'title': "whatever",
+ 'footer': {
+ 'text': "django good"
+ }
+ })
+
+ def test_rejects_author_as_non_mapping(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'title': "whatever",
+ 'author': []
+ })
+
+ def test_rejects_author_with_unknown_field(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'title': "whatever",
+ 'author': {
+ 'field': "that is unknown"
+ }
+ })
+
+ def test_rejects_author_with_empty_name(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'title': "whatever",
+ 'author': {
+ 'name': ""
+ }
+ })
+
+ def test_rejects_author_with_one_correct_one_incorrect(self):
+ with self.assertRaises(ValidationError):
+ validate_tag_embed({
+ 'title': "whatever",
+ 'author': {
+ # Relies on "dictionary insertion order remembering" (D.I.O.R.) behaviour
+ 'url': "bobswebsite.com",
+ 'name': ""
+ }
+ })
+
+ def test_allows_author_with_proper_values(self):
+ validate_tag_embed({
+ 'title': "whatever",
+ 'author': {
+ 'name': "Bob"
+ }
+ })