aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pysite/tables.py6
-rw-r--r--pysite/views/api/bot/off_topic_names.py83
-rw-r--r--tests/test_api_bot_off_topic_names.py90
3 files changed, 179 insertions, 0 deletions
diff --git a/pysite/tables.py b/pysite/tables.py
index de9499e8..784183f8 100644
--- a/pysite/tables.py
+++ b/pysite/tables.py
@@ -138,6 +138,12 @@ TABLES = {
])
),
+ "off_topic_names": Table( # Names for the off-topic category channels
+ primary_key="name",
+ keys=("name",),
+ locked=False
+ ),
+
"snake_facts": Table( # Snake facts
primary_key="fact",
keys=sorted([
diff --git a/pysite/views/api/bot/off_topic_names.py b/pysite/views/api/bot/off_topic_names.py
new file mode 100644
index 00000000..e0871067
--- /dev/null
+++ b/pysite/views/api/bot/off_topic_names.py
@@ -0,0 +1,83 @@
+import random
+
+from flask import jsonify, request
+from schema import And, Schema
+
+from pysite.base_route import APIView
+from pysite.constants import ValidationTypes
+from pysite.decorators import api_key, api_params
+from pysite.mixins import DBMixin
+
+
+POST_SCHEMA = Schema({
+ 'name': And(
+ str,
+ len,
+ lambda name: all(c.isalpha() or c == '-' for c in name),
+ str.islower,
+ lambda name: len(name) <= 96,
+ error=(
+ "The channel name must be a non-blank string consisting only of"
+ " lowercase regular characters and '-' with a maximum length of 96"
+ )
+ )
+})
+
+
+class OffTopicNamesView(APIView, DBMixin):
+ path = "/bot/off-topic-names"
+ name = "bot.off_topic_names"
+ table_name = "off_topic_names"
+
+ @api_key
+ def get(self):
+ """
+ Fetch all known off-topic channel names from the database.
+ Returns a list of strings, the strings being the off-topic names.
+
+ If the query argument `random_items` is provided (a non-negative integer),
+ then this view will return `random_items` random names from the database
+ instead of returning all items at once.
+
+ API key must be provided as header.
+ """
+
+ names = [
+ entry['name'] for entry in self.db.get_all(self.table_name)
+ ]
+
+ if 'random_items' in request.args:
+ random_count = request.args['random_items']
+ if not random_count.isdigit():
+ response = {'message': "`random_items` must be a valid integer"}
+ return jsonify(response), 400
+
+ samples = random.sample(names, int(random_count))
+ return jsonify(samples)
+
+ return jsonify(names)
+
+ @api_key
+ @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.params)
+ def post(self, data):
+ """
+ Add a new off-topic channel name to the database.
+ Expects the new channel's name as the `name` argument.
+ The name must consist only of alphanumeric characters or minus signs,
+ and must not be empty or exceed 96 characters.
+
+ Data must be provided as params.
+ API key must be provided as header.
+ """
+
+ if self.db.get(self.table_name, data['name']) is not None:
+ response = {
+ 'message': "An entry with the given name already exists"
+ }
+ return jsonify(response), 400
+
+ self.db.insert(
+ self.table_name,
+ {'name': data['name']}
+ )
+ return jsonify({'message': 'ok'})
diff --git a/tests/test_api_bot_off_topic_names.py b/tests/test_api_bot_off_topic_names.py
new file mode 100644
index 00000000..4c9c782b
--- /dev/null
+++ b/tests/test_api_bot_off_topic_names.py
@@ -0,0 +1,90 @@
+"""Tests the `/api/bot/off-topic-names` endpoint."""
+
+from tests import SiteTest, app
+
+
+class EmptyDatabaseOffTopicEndpointTests(SiteTest):
+ """Tests fetching all entries from the endpoint with an empty database."""
+
+ def test_get_returns_empty_list(self):
+ response = self.client.get(
+ '/bot/off-topic-names',
+ app.config['API_SUBDOMAIN'],
+ headers=app.config['TEST_HEADER']
+ )
+ self.assert200(response)
+ self.assertEqual(response.json, [])
+
+
+class AddingANameOffTopicEndpointTests(SiteTest):
+ """Tests adding a channel name to the database."""
+
+ def test_returns_400_on_bad_data(self):
+ response = self.client.post(
+ '/bot/off-topic-names?name=my%20TOTALLY%20VALID%20CHANNE%20NAME',
+ app.config['API_SUBDOMAIN'],
+ headers=app.config['TEST_HEADER']
+ )
+ self.assert400(response)
+
+ def test_can_add_new_package(self):
+ response = self.client.post(
+ '/bot/off-topic-names?name=lemons-lemon-shop',
+ app.config['API_SUBDOMAIN'],
+ headers=app.config['TEST_HEADER']
+ )
+ self.assert200(response)
+
+
+class AddingChannelNameToDatabaseEndpointTests(SiteTest):
+ """Tests fetching names from the database with GET."""
+
+ CHANNEL_NAME = 'bisks-disks'
+
+ def setUp(self):
+ response = self.client.post(
+ f'/bot/off-topic-names?name={self.CHANNEL_NAME}',
+ app.config['API_SUBDOMAIN'],
+ headers=app.config['TEST_HEADER']
+ )
+ self.assert200(response)
+
+ def test_name_is_in_all_entries(self):
+ response = self.client.get(
+ '/bot/off-topic-names',
+ app.config['API_SUBDOMAIN'],
+ headers=app.config['TEST_HEADER']
+ )
+ self.assert200(response)
+ self.assertIn(self.CHANNEL_NAME, response.json)
+
+
+class RandomSampleEndpointTests(SiteTest):
+ """Tests fetching random names from the website with GET."""
+
+ CHANNEL_NAME_1 = 'chicken-shed'
+ CHANNEL_NAME_2 = 'robot-kindergarten'
+
+ def setUp(self):
+ response = self.client.post(
+ f'/bot/off-topic-names?name={self.CHANNEL_NAME_1}',
+ app.config['API_SUBDOMAIN'],
+ headers=app.config['TEST_HEADER']
+ )
+ self.assert200(response)
+
+ response = self.client.post(
+ f'/bot/off-topic-names?name={self.CHANNEL_NAME_2}',
+ app.config['API_SUBDOMAIN'],
+ headers=app.config['TEST_HEADER']
+ )
+ self.assert200(response)
+
+ def test_returns_limited_names_with_random_query_param(self):
+ response = self.client.get(
+ '/bot/off-topic-names?random_items=1',
+ app.config['API_SUBDOMAIN'],
+ headers=app.config['TEST_HEADER']
+ )
+ self.assert200(response)
+ self.assertEqual(len(response.json), 1)