aboutsummaryrefslogtreecommitdiffstats
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/migrations/0002_documentationlink.py21
-rw-r--r--api/models.py10
-rw-r--r--api/serializers.py8
-rw-r--r--api/tests/test_documentation_links.py161
-rw-r--r--api/urls.py4
-rw-r--r--api/viewsets.py87
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.
"""