diff options
author | 2019-04-05 18:24:32 +0100 | |
---|---|---|
committer | 2019-04-05 18:24:32 +0100 | |
commit | ab8b798547e82ca79882ba28b1920077c803425f (patch) | |
tree | b28a9005d05dfd2c9da62351671bf2aa6e37f7dc /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__.py | 0 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/base.py | 69 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_deleted_messages.py | 43 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_documentation_links.py | 161 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_healthcheck.py | 16 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_infractions.py | 359 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_models.py | 113 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_nominations.py | 41 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_off_topic_channel_names.py | 152 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_rules.py | 35 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_snake_names.py | 67 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_users.py | 121 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_validators.py | 213 |
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', + email='[email protected]', + 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" + } + }) |