diff options
Diffstat (limited to 'api')
-rw-r--r-- | api/migrations/0002_documentationlink.py | 21 | ||||
-rw-r--r-- | api/models.py | 10 | ||||
-rw-r--r-- | api/serializers.py | 8 | ||||
-rw-r--r-- | api/tests/test_documentation_links.py | 161 | ||||
-rw-r--r-- | api/urls.py | 4 | ||||
-rw-r--r-- | api/viewsets.py | 87 |
6 files changed, 280 insertions, 11 deletions
diff --git a/api/migrations/0002_documentationlink.py b/api/migrations/0002_documentationlink.py new file mode 100644 index 00000000..5dee679a --- /dev/null +++ b/api/migrations/0002_documentationlink.py @@ -0,0 +1,21 @@ +# Generated by Django 2.1 on 2018-08-16 19:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='DocumentationLink', + fields=[ + ('package', models.CharField(max_length=50, primary_key=True, serialize=False)), + ('base_url', models.URLField()), + ('inventory_url', models.URLField()), + ], + ), + ] diff --git a/api/models.py b/api/models.py index 6d07beff..877e3622 100644 --- a/api/models.py +++ b/api/models.py @@ -1,6 +1,16 @@ from django.db import models +class DocumentationLink(models.Model): + """A documentation link used by the `!docs` command of the bot.""" + + package = models.CharField(primary_key=True, max_length=50) + base_url = models.URLField() + inventory_url = models.URLField() + + class SnakeName(models.Model): + """A snake name used by the bot's snake cog.""" + name = models.CharField(primary_key=True, max_length=100) scientific = models.CharField(max_length=150) diff --git a/api/serializers.py b/api/serializers.py index cac94873..1e18dfef 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,6 +1,12 @@ from rest_framework.serializers import ModelSerializer -from .models import SnakeName +from .models import DocumentationLink, SnakeName + + +class DocumentationLinkSerializer(ModelSerializer): + class Meta: + model = DocumentationLink + fields = ('package', 'base_url', 'inventory_url') class SnakeNameSerializer(ModelSerializer): diff --git a/api/tests/test_documentation_links.py b/api/tests/test_documentation_links.py new file mode 100644 index 00000000..e560a2fd --- /dev/null +++ b/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): + 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): + 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/api/urls.py b/api/urls.py index b0e104cd..78a310f6 100644 --- a/api/urls.py +++ b/api/urls.py @@ -2,13 +2,15 @@ from django.urls import include, path from rest_framework.routers import SimpleRouter from .views import HealthcheckView -from .viewsets import SnakeNameViewSet +from .viewsets import DocumentationLinkViewSet, SnakeNameViewSet # http://www.django-rest-framework.org/api-guide/routers/#simplerouter bot_router = SimpleRouter(trailing_slash=False) +bot_router.register(r'documentation-links', DocumentationLinkViewSet) bot_router.register(r'snake-names', SnakeNameViewSet, base_name='snakename') + app_name = 'api' urlpatterns = ( # Build URLs using something like... diff --git a/api/viewsets.py b/api/viewsets.py index 5e38bdc9..39721847 100644 --- a/api/viewsets.py +++ b/api/viewsets.py @@ -1,33 +1,102 @@ +from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin from rest_framework.response import Response -from rest_framework.viewsets import ViewSet +from rest_framework.viewsets import GenericViewSet, ViewSet -from .models import SnakeName -from .serializers import SnakeNameSerializer +from .models import DocumentationLink, SnakeName +from .serializers import DocumentationLinkSerializer, SnakeNameSerializer + + +class DocumentationLinkViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): + """ + View providing management of documentation links used in the bot's `Doc` cog. + + ## Routes + ### GET /bot/documentation-links + Retrieve all currently stored entries from the database. + + #### Response format + >>> [ + ... { + ... 'package': 'flask', + ... 'base_url': 'https://flask.pocoo.org/docs/dev', + ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv' + ... }, + ... # ... + ... ] + + #### Status codes + - 200: returned on success + + ### GET /bot/documentation-links/<package:str> + Look up the documentation object for the given `package`. + + #### Response format + >>> { + ... 'package': 'flask', + ... 'base_url': 'https://flask.pocoo.org/docs/dev', + ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv' + ... } + + #### Status codes + - 200: returned on success + - 404: if no entry for the given `package` exists + + ### POST /bot/documentation-links + Create a new documentation link object. + + #### Body schema + >>> { + ... 'package': str, + ... 'base_url': URL, + ... 'inventory_url': URL + ... } + + #### Status codes + - 201: returned on success + - 400: if the request body has invalid fields, see the response for details + + ### DELETE /bot/documentation-links/<package:str> + Delete the entry for the given `package`. + + #### Status codes + - 204: returned on success + - 404: if the given `package` could not be found + """ + + queryset = DocumentationLink.objects.all() + serializer_class = DocumentationLinkSerializer + lookup_field = 'package' class SnakeNameViewSet(ViewSet): """ - View of snake names for the bot's snake cog from our first code jam's winners. + View providing snake names for the bot's snake cog from our first code jam's winners. ## Routes ### GET /bot/snake-names - By default, return a single random snake name as JSON in the following format: + By default, return a single random snake name along with its name and scientific name. + If the `get_all` query parameter is given, for example using... + $ curl api.pythondiscord.local:8000/bot/snake-names?get_all=yes + ...then the API will return all snake names and scientific names in the database. + #### Response format + Without `get_all` query parameter: >>> { ... 'name': "Python", ... 'scientific': "Langus greatus" ... } - If the `get_all` query parameter is given, for example using... - $ curl api.pythondiscord.local:8000/bot/snake-names?get_all=yes - ...then the API will return all snake names and scientific names in the database, - for example: + If the database is empty for whatever reason, this will return an empty dictionary. + With `get_all` query parameter: >>> [ ... {'name': "Python 3", 'scientific': "Langus greatus"}, ... {'name': "Python 2", 'scientific': "Langus decentus"} ... ] + #### Status codes + - 200: returned on success + ## Authentication Requires a API token. """ |