aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.coveragerc3
-rw-r--r--app_test.py81
-rw-r--r--pysite/base_route.py3
-rw-r--r--pysite/database.py30
-rw-r--r--pysite/decorators.py1
-rw-r--r--pysite/route_manager.py2
-rw-r--r--pysite/views/api/bot/tag.py57
-rw-r--r--pysite/views/api/bot/tags.py116
-rw-r--r--pysite/views/api/error_view.py40
9 files changed, 254 insertions, 79 deletions
diff --git a/.coveragerc b/.coveragerc
index 89207693..aee4d336 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,2 +1,5 @@
[run]
omit = /usr/*, gunicorn_config.py, deploy.py, app_test.py, app.py, pysite/websockets.py, pysite/views/*__init__.py, pysite/route_manager.py
+
+[report]
+exclude_lines = return jsonify
diff --git a/app_test.py b/app_test.py
index eb94179e..2176fe08 100644
--- a/app_test.py
+++ b/app_test.py
@@ -13,21 +13,25 @@ app = manager.app
class SiteTest(TestCase):
- """ extend TestCase with flask app instantiation """
+ """ Extend TestCase with flask app instantiation """
+
def create_app(self):
- """ add flask app configuration settings """
+ """ Add flask app configuration settings """
server_name = 'pytest.local'
+
app.config['TESTING'] = True
app.config['LIVESERVER_TIMEOUT'] = 10
app.config['SERVER_NAME'] = server_name
app.config['API_SUBDOMAIN'] = f'http://api.{server_name}'
app.config['STAFF_SUBDOMAIN'] = f'http://staff.{server_name}'
app.allow_subdomain_redirects = True
+
return app
-class BaseEndpoints(SiteTest):
- """ test cases for the base endpoints """
+class RootEndpoint(SiteTest):
+ """ Test cases for the root endpoint and error handling """
+
def test_index(self):
""" Check the root path reponds with 200 OK """
response = self.client.get('/', 'http://pytest.local')
@@ -104,35 +108,74 @@ class ApiEndpoints(SiteTest):
self.assertEqual(response.json, {'status': 'ok'})
self.assertEqual(response.status_code, 200)
- def test_api_tag(self):
- """ Check tag api """
+ def test_api_tags(self):
+ """ Check tag API """
os.environ['BOT_API_KEY'] = 'abcdefg'
headers = {'X-API-Key': 'abcdefg', 'Content-Type': 'application/json'}
- good_data = json.dumps({
+
+ post_data = json.dumps({
'tag_name': 'testing',
- 'tag_content': 'testing',
- 'tag_category': 'testing'})
+ 'tag_content': 'testing'
+ })
+
+ get_data = json.dumps({
+ 'tag_name': 'testing'
+ })
bad_data = json.dumps({
- 'not_a_valid_key': 'testing',
- 'tag_content': 'testing',
- 'tag_category': 'testing'})
+ 'not_a_valid_key': 'gross_faceman'
+ })
+
+ # POST method - no headers
+ response = self.client.post('/tags', app.config['API_SUBDOMAIN'])
+ self.assertEqual(response.status_code, 401)
+
+ # POST method - no data
+ response = self.client.post('/tags', app.config['API_SUBDOMAIN'], headers=headers)
+ self.assertEqual(response.status_code, 400)
+
+ # POST method - bad data
+ response = self.client.post('/tags', app.config['API_SUBDOMAIN'], headers=headers, data=bad_data)
+ self.assertEqual(response.status_code, 400)
- response = self.client.get('/tag', app.config['API_SUBDOMAIN'])
+ # POST method - save tag
+ response = self.client.post('/tags', app.config['API_SUBDOMAIN'], headers=headers, data=post_data)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json, {"success": True})
+
+ # GET method - no headers
+ response = self.client.get('/tags', app.config['API_SUBDOMAIN'])
self.assertEqual(response.status_code, 401)
- response = self.client.get('/tag', app.config['API_SUBDOMAIN'], headers=headers)
+ # GET method - get all tags
+ response = self.client.get('/tags', app.config['API_SUBDOMAIN'], headers=headers)
self.assertEqual(response.status_code, 200)
+ self.assertEqual(type(response.json), list)
- response = self.client.post('/tag', app.config['API_SUBDOMAIN'], headers=headers, data=bad_data)
+ # GET method - get specific tag
+ response = self.client.get('/tags?tag_name=testing', app.config['API_SUBDOMAIN'], headers=headers)
+ self.assertEqual(response.json, {
+ 'tag_content': 'testing',
+ 'tag_name': 'testing'
+ })
+ self.assertEqual(response.status_code, 200)
+
+ # DELETE method - no headers
+ response = self.client.delete('/tags', app.config['API_SUBDOMAIN'])
+ self.assertEqual(response.status_code, 401)
+
+ # DELETE method - no data
+ response = self.client.delete('/tags', app.config['API_SUBDOMAIN'], headers=headers)
self.assertEqual(response.status_code, 400)
- response = self.client.post('/tag', app.config['API_SUBDOMAIN'], headers=headers, data=good_data)
- self.assertEqual(response.json, {'success': True})
+ # DELETE method - bad data
+ response = self.client.delete('/tags', app.config['API_SUBDOMAIN'], headers=headers, data=bad_data)
+ self.assertEqual(response.status_code, 400)
- response = self.client.get('/tag', app.config['API_SUBDOMAIN'], headers=headers, data=good_data)
- self.assertEqual(response.json, [{'tag_name': 'testing'}])
+ # DELETE method - delete the testing tag
+ response = self.client.delete('/tags', app.config['API_SUBDOMAIN'], headers=headers, data=get_data)
self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json, {"success": True})
def test_api_user(self):
""" Check insert user """
diff --git a/pysite/base_route.py b/pysite/base_route.py
index f389b56e..4e1a63a7 100644
--- a/pysite/base_route.py
+++ b/pysite/base_route.py
@@ -86,11 +86,12 @@ class APIView(RouteView):
... return self.error(ErrorCodes.unknown_route)
"""
- def error(self, error_code: ErrorCodes) -> Response:
+ def error(self, error_code: ErrorCodes, error_info: str = "") -> Response:
"""
Generate a JSON response for you to return from your handler, for a specific type of API error
:param error_code: The type of error to generate a response for - see `constants.ErrorCodes` for more
+ :param error_info: An optional message with more information about the error.
:return: A Flask Response object that you can return from your handler
"""
diff --git a/pysite/database.py b/pysite/database.py
index 78c4368a..add76923 100644
--- a/pysite/database.py
+++ b/pysite/database.py
@@ -103,6 +103,34 @@ class RethinkDB:
self.log.debug(f"Table created: '{table_name}'")
return True
+ def delete(self, table_name: str, primary_key: Optional[str] = None,
+ durability: str = "hard", return_changes: Union[bool, str] = False
+ ) -> Union[Dict[str, Any], None]:
+ """
+ Delete one or all documents from a table. This can only delete
+ either the contents of an entire table, or a single document.
+ For more complex delete operations, please use self.query.
+
+ :param table_name: The name of the table to delete from. This must be provided.
+ :param primary_key: The primary_key to delete from that table. This is optional.
+ :param durability: "hard" (the default) to write the change immediately, "soft" otherwise
+ :param return_changes: Whether to return a list of changed values or not - defaults to False
+ :return: if return_changes is True, returns a dict containing all changes. Else, returns None.
+ """
+
+ if primary_key:
+ query = self.query(table_name).get(primary_key).delete(
+ durability=durability, return_changes=return_changes
+ )
+ else:
+ query = self.query(table_name).delete(
+ durability=durability, return_changes=return_changes
+ )
+
+ if return_changes:
+ return self.run(query, coerce=dict)
+ self.run(query)
+
def drop_table(self, table_name: str):
"""
Attempt to drop a table from the database, along with its data
@@ -168,7 +196,7 @@ class RethinkDB:
:param connect_database: If creating a new connection, whether to connect to the database immediately
:param coerce: Optionally, an object type to attempt to coerce the result to
- :return: THe result of the operation
+ :return: The result of the operation
"""
if not new_connection:
diff --git a/pysite/decorators.py b/pysite/decorators.py
index 952c2349..d8bf1381 100644
--- a/pysite/decorators.py
+++ b/pysite/decorators.py
@@ -48,6 +48,7 @@ def api_params(schema: Schema, validation_type: ValidationTypes = ValidationType
if not isinstance(data, list):
data = [data]
+
except JSONDecodeError:
return self.error(ErrorCodes.bad_data_format) # pragma: no cover
diff --git a/pysite/route_manager.py b/pysite/route_manager.py
index 53b24def..72517a3c 100644
--- a/pysite/route_manager.py
+++ b/pysite/route_manager.py
@@ -27,7 +27,7 @@ class RouteManager:
self.db = RethinkDB()
self.log = logging.getLogger()
self.app.secret_key = os.environ.get("WEBPAGE_SECRET_KEY", "super_secret")
- self.app.config["SERVER_NAME"] = os.environ.get("SERVER_NAME", "pythondiscord.com:8080")
+ self.app.config["SERVER_NAME"] = os.environ.get("SERVER_NAME", "pythondiscord.local:8080")
self.app.before_request(self.db.before_request)
self.app.teardown_request(self.db.teardown_request)
diff --git a/pysite/views/api/bot/tag.py b/pysite/views/api/bot/tag.py
deleted file mode 100644
index 8818074e..00000000
--- a/pysite/views/api/bot/tag.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# coding=utf-8
-
-from flask import jsonify, request
-
-from pysite.base_route import APIView
-from pysite.constants import ErrorCodes
-from pysite.decorators import api_key
-from pysite.mixins import DBMixin
-
-
-class TagView(APIView, DBMixin):
- path = "/tag"
- name = "tag"
- table_name = "tag"
- table_primary_key = "tag_name"
-
- @api_key
- def get(self):
- """
- Data must be provided as params,
- API key must be provided as header
- """
-
- tag_name = request.args.get("tag_name")
-
- if tag_name:
- data = self.db.get(self.table_name, tag_name) or {} # pragma: no cover
- else:
- data = self.db.pluck(self.table_name, "tag_name") or []
-
- return jsonify(data) # pragma: no cover
-
- @api_key
- def post(self):
- """
- Data must be provided as JSON.
- """
-
- data = request.get_json()
-
- tag_name = data.get("tag_name")
- tag_content = data.get("tag_content")
- tag_category = data.get("tag_category")
-
- if tag_name and tag_content:
- self.db.insert(
- self.table_name,
- {
- "tag_name": tag_name,
- "tag_content": tag_content,
- "tag_category": tag_category
- }
- )
- else:
- return self.error(ErrorCodes.incorrect_parameters)
-
- return jsonify({"success": True}) # pragma: no cover
diff --git a/pysite/views/api/bot/tags.py b/pysite/views/api/bot/tags.py
new file mode 100644
index 00000000..9db926f4
--- /dev/null
+++ b/pysite/views/api/bot/tags.py
@@ -0,0 +1,116 @@
+# coding=utf-8
+
+from flask import jsonify
+from schema import Schema, Optional
+
+from pysite.base_route import APIView
+from pysite.constants import ValidationTypes
+from pysite.decorators import api_key, api_params
+from pysite.mixins import DBMixin
+
+GET_SCHEMA = Schema([
+ {
+ Optional("tag_name"): str
+ }
+])
+
+POST_SCHEMA = Schema([
+ {
+ "tag_name": str,
+ "tag_content": str
+ }
+])
+
+DELETE_SCHEMA = Schema([
+ {
+ "tag_name": str
+ }
+])
+
+
+class TagsView(APIView, DBMixin):
+ path = "/tags"
+ name = "tags"
+ table_name = "tags"
+ table_primary_key = "tag_name"
+
+ @api_key
+ @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params)
+ def get(self, params=None):
+ """
+ Fetches tags from the database.
+
+ - If tag_name is provided, it fetches
+ that specific tag.
+
+ - If tag_category is provided, it fetches
+ all tags in that category.
+
+ - If nothing is provided, it will
+ fetch a list of all tag_names.
+
+ Data must be provided as params.
+ API key must be provided as header.
+ """
+
+ tag_name = None
+
+ if params:
+ tag_name = params[0].get("tag_name")
+
+ if tag_name:
+ data = self.db.get(self.table_name, tag_name) or {}
+ else:
+ data = self.db.pluck(self.table_name, "tag_name") or []
+
+ return jsonify(data)
+
+ @api_key
+ @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json)
+ def post(self, json_data):
+ """
+ If the tag_name doesn't exist, this
+ saves a new tag in the database.
+
+ If the tag_name already exists,
+ this will edit the existing tag.
+
+ Data must be provided as JSON.
+ API key must be provided as header.
+ """
+
+ json_data = json_data[0]
+
+ tag_name = json_data.get("tag_name")
+ tag_content = json_data.get("tag_content")
+
+ self.db.insert(
+ self.table_name,
+ {
+ "tag_name": tag_name,
+ "tag_content": tag_content
+ },
+ conflict="update" # If it exists, update it.
+ )
+
+ return jsonify({"success": True})
+
+ @api_key
+ @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json)
+ def delete(self, data):
+ """
+ Deletes a tag from the database.
+
+ Data must be provided as JSON.
+ API key must be provided as header.
+ """
+
+ json = data[0]
+ tag_name = json.get("tag_name")
+
+ self.db.delete(
+ self.table_name,
+ tag_name
+ )
+
+ return jsonify({"success": True})
diff --git a/pysite/views/api/error_view.py b/pysite/views/api/error_view.py
new file mode 100644
index 00000000..e5301336
--- /dev/null
+++ b/pysite/views/api/error_view.py
@@ -0,0 +1,40 @@
+# coding=utf-8
+from flask import jsonify
+from werkzeug.exceptions import HTTPException
+
+from pysite.base_route import ErrorView
+
+
+class APIErrorView(ErrorView):
+ name = "api_error_all"
+ error_code = range(400, 600)
+
+ def __init__(self):
+
+ # Direct errors for all methods at self.return_error
+ methods = [
+ 'get', 'post', 'put',
+ 'delete', 'patch', 'connect',
+ 'options', 'trace'
+ ]
+
+ for method in methods:
+ setattr(self, method, self.return_error)
+
+ def return_error(self, error: HTTPException):
+ """
+ Return a basic JSON object representing the HTTP error,
+ as well as propagating its status code
+ """
+
+ message = str(error)
+ code = 500
+
+ if isinstance(error, HTTPException):
+ message = error.description
+ code = error.code
+
+ return jsonify({
+ "error_code": -1,
+ "error_message": message
+ }), code