aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.coveragerc2
-rw-r--r--app_test.py227
-rw-r--r--pysite/base_route.py4
-rw-r--r--pysite/database.py24
-rw-r--r--pysite/decorators.py9
-rw-r--r--pysite/mixins.py4
-rw-r--r--pysite/views/api/bot/tag.py6
-rw-r--r--pysite/views/api/bot/user.py2
-rw-r--r--pysite/views/tests/__init__.py1
-rw-r--r--pysite/views/tests/index.py23
10 files changed, 257 insertions, 45 deletions
diff --git a/.coveragerc b/.coveragerc
index 630326b5..58b972ee 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,2 +1,2 @@
[run]
-omit = /usr/*, gunicorn_config.py, deploy.py, app_test.py, app.py
+omit = /usr/*, gunicorn_config.py, deploy.py, app_test.py, app.py, pysite/views/*__init__.py, pysite/route_manager.py
diff --git a/app_test.py b/app_test.py
index 896a47cd..97e38f4d 100644
--- a/app_test.py
+++ b/app_test.py
@@ -1,15 +1,22 @@
-import json # pragma: no cover
-import os # pragma: no cover
+import json
+import os
-from app import app
+from app import manager
-from flask_testing import TestCase # pragma: no cover
+from flask import Blueprint
+
+from flask_testing import TestCase
+
+manager.app.tests_blueprint = Blueprint("tests", __name__)
+manager.load_views(manager.app.tests_blueprint, "pysite/views/tests")
+manager.app.register_blueprint(manager.app.tests_blueprint)
+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
@@ -20,37 +27,45 @@ class SiteTest(TestCase):
return app
-class RootEndpoint(SiteTest):
- ''' test cases for the root endpoint and error handling '''
+class BaseEndpoints(SiteTest):
+ """ test cases for the base endpoints """
def test_index(self):
- ''' Check the root path reponds with 200 OK '''
+ """ Check the root path reponds with 200 OK """
response = self.client.get('/', 'http://pytest.local')
self.assertEqual(response.status_code, 200)
- def test_not_found(self):
- ''' Check paths without handlers returns 404 Not Found '''
- response = self.client.get('/nonexistentpath')
- self.assertEqual(response.status_code, 404)
-
def test_invite(self):
- ''' Check invite redirects '''
+ """ Check invite redirects """
response = self.client.get('/invite')
self.assertEqual(response.status_code, 302)
+ def test_ws_test(self):
+ """ check ws_test responds """
+ response = self.client.get('/ws_test')
+ self.assertEqual(response.status_code, 200)
+
+ def test_datadog_redirect(self):
+ """ Check datadog path redirects """
+ response = self.client.get('/datadog')
+ self.assertEqual(response.status_code, 302)
+
+
+class ApiEndpoints(SiteTest):
+ """ test cases for the api subdomain """
def test_api_unknown_route(self):
- ''' Check api unknown route '''
+ """ Check api unknown route """
response = self.client.get('/', app.config['API_SUBDOMAIN'])
self.assertEqual(response.json, {'error_code': 0, 'error_message': 'Unknown API route'})
self.assertEqual(response.status_code, 404)
def test_api_healthcheck(self):
- ''' Check healthcheck url responds '''
+ """ Check healthcheck url responds """
response = self.client.get('/healthcheck', app.config['API_SUBDOMAIN'])
self.assertEqual(response.json, {'status': 'ok'})
self.assertEqual(response.status_code, 200)
def test_api_tag(self):
- ''' Check tag api '''
+ """ Check tag api """
os.environ['BOT_API_KEY'] = 'abcdefg'
headers = {'X-API-Key': 'abcdefg', 'Content-Type': 'application/json'}
good_data = json.dumps({
@@ -80,7 +95,7 @@ class RootEndpoint(SiteTest):
self.assertEqual(response.status_code, 200)
def test_api_user(self):
- ''' Check insert user '''
+ """ Check insert user """
os.environ['BOT_API_KEY'] = 'abcdefg'
headers = {'X-API-Key': 'abcdefg', 'Content-Type': 'application/json'}
bad_data = json.dumps({'user_id': 1234, 'role': 5678})
@@ -94,3 +109,177 @@ class RootEndpoint(SiteTest):
response = self.client.post('/user', app.config['API_SUBDOMAIN'], headers=headers, data=good_data)
self.assertEqual(True, "inserted" in response.json)
+
+ def test_api_route_errors(self):
+ """ Check api route errors """
+ from pysite.base_route import APIView
+ from pysite.constants import ErrorCodes
+
+ av = APIView()
+ av.error(ErrorCodes.unauthorized)
+ av.error(ErrorCodes.bad_data_format)
+
+ def test_not_found(self):
+ """ Check paths without handlers returns 404 Not Found """
+ response = self.client.get('/nonexistentpath')
+ self.assertEqual(response.status_code, 404)
+
+
+class StaffEndpoints(SiteTest):
+ """ Test cases for staff subdomain """
+ def test_staff_view(self):
+ """ Check staff view renders """
+ from pysite.views.staff.index import StaffView
+ sv = StaffView()
+ result = sv.get()
+ self.assertEqual(result.startswith('<!DOCTYPE html>'), True)
+
+ response = self.client.get('/', app.config['STAFF_SUBDOMAIN'])
+ self.assertEqual(response.status_code, 200)
+
+
+class Utilities(SiteTest):
+ """ Test cases for internal utility code """
+ def test_logging_runtime_error(self):
+ """ Check that a wrong value for log level raises runtime error """
+ os.environ['LOG_LEVEL'] = 'wrong value'
+ try:
+ import pysite.__init__ # noqa: F401
+ except RuntimeError:
+ return True
+ finally:
+ os.environ['LOG_LEVEL'] = 'info'
+ raise Exception('Expected a failure due to wrong LOG_LEVEL attribute name')
+
+ def test_error_view_runtime_error(self):
+ """ Check that wrong values for error view setup raises runtime error """
+ import pysite.base_route
+
+ ev = pysite.base_route.ErrorView()
+ try:
+ ev.setup('sdf', 'sdfsdf')
+ except RuntimeError:
+ return True
+ raise Exception('Expected runtime error on setup() when giving wrongful arguments')
+
+
+class MixinTests(SiteTest):
+ """ Test cases for mixins """
+ def test_dbmixin_runtime_error(self):
+ """ Check that wrong values for error view setup raises runtime error """
+ from pysite.mixins import DBMixin
+
+ dbm = DBMixin()
+ try:
+ dbm.setup('sdf', 'sdfsdf')
+ except RuntimeError:
+ return True
+ raise Exception('Expected runtime error on setup() when giving wrongful arguments')
+
+ def test_dbmixin_table_property(self):
+ """ Check the table property returns correctly """
+ from pysite.mixins import DBMixin
+
+ try:
+ dbm = DBMixin()
+ dbm.table_name = 'Table'
+ self.assertEquals(dbm.table, 'Table')
+ except AttributeError:
+ pass
+
+ def test_handler_5xx(self):
+ """ Check error view returns error message """
+ from werkzeug.exceptions import InternalServerError
+ from pysite.views.error_handlers import http_5xx
+
+ error_view = http_5xx.Error404View()
+ error_message = error_view.get(InternalServerError)
+ self.assertEqual(error_message, ('Internal server error. Please try again later!', 500))
+
+ def test_route_view_runtime_error(self):
+ """ Check that wrong values for route view setup raises runtime error """
+ from pysite.base_route import RouteView
+
+ rv = RouteView()
+ try:
+ rv.setup('sdf', 'sdfsdf')
+ except RuntimeError:
+ return True
+ raise Exception('Expected runtime error on setup() when giving wrongful arguments')
+
+ def test_route_manager(self):
+ """ Check route manager """
+ from pysite.route_manager import RouteManager
+ os.environ['WEBPAGE_SECRET_KEY'] = 'super_secret'
+ rm = RouteManager()
+ self.assertEqual(rm.app.secret_key, 'super_secret')
+
+
+class DecoratorTests(SiteTest):
+ def test_decorator_api_json(self):
+ """ Check the json validation decorator """
+ from pysite.decorators import api_params
+ from pysite.constants import ValidationTypes
+ from schema import Schema
+
+ SCHEMA = Schema([{"user_id": int, "role": int}])
+
+ @api_params(schema=SCHEMA, validation_type=ValidationTypes.json)
+ def try_json_type(data):
+ return data
+
+ try:
+ try_json_type("not json")
+ except Exception as error_message:
+ self.assertEqual(type(error_message), AttributeError)
+
+ def test_decorator_params(self):
+ """ Check the params validation decorator """
+
+ response = self.client.post('/testparams?test=params')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json, [{'test': 'params'}])
+
+
+class DatabaseTests(SiteTest):
+ """ Test cases for the database module """
+ def test_table_actions(self):
+ import string
+ import secrets
+ from pysite.database import RethinkDB
+
+ alphabet = string.ascii_letters
+ generated_table_name = ''.join(secrets.choice(alphabet) for i in range(8))
+
+ rdb = RethinkDB()
+ # Create table name and expect it to work
+ result = rdb.create_table(generated_table_name)
+ self.assertEquals(result, True)
+
+ # Create the same table name and expect it to already exist
+ result = rdb.create_table(generated_table_name)
+ self.assertEquals(result, False)
+
+ # Drop table and expect it to work
+ result = rdb.drop_table(generated_table_name)
+ self.assertEquals(result, True)
+
+ # Drop the same table and expect it to already be gone
+ result = rdb.drop_table(generated_table_name)
+ self.assertEquals(result, False)
+
+ # This is to get some more code coverage
+ self.assertEquals(rdb.teardown_request('_'), None)
+
+
+class TestWebsocketEcho(SiteTest):
+ """ Test cases for the echo endpoint """
+ def testEcho(self):
+ """ Check rudimentary websockets handlers work """
+ from geventwebsocket.websocket import WebSocket
+ from pysite.views.ws.echo import EchoWebsocket
+ ew = EchoWebsocket(WebSocket)
+ ew.on_open()
+ ew.on_message('message')
+ ew.on_close()
diff --git a/pysite/base_route.py b/pysite/base_route.py
index 017a8b6e..400a4649 100644
--- a/pysite/base_route.py
+++ b/pysite/base_route.py
@@ -156,7 +156,7 @@ class ErrorView(BaseView):
"""
if hasattr(super(), "setup"):
- super().setup(manager, blueprint)
+ super().setup(manager, blueprint) # pragma: no cover
if not cls.name or not cls.error_code:
raise RuntimeError("Error views must have both `name` and `error_code` defined")
@@ -171,4 +171,4 @@ class ErrorView(BaseView):
except KeyError: # This happens if we try to register a handler for a HTTP code that doesn't exist
pass
else:
- raise RuntimeError("Error views must have an `error_code` that is either an `int` or an iterable")
+ raise RuntimeError("Error views must have an `error_code` that is either an `int` or an iterable") # pragma: no cover # noqa: E501
diff --git a/pysite/database.py b/pysite/database.py
index f031e2a8..239a2fdc 100644
--- a/pysite/database.py
+++ b/pysite/database.py
@@ -228,11 +228,11 @@ class RethinkDB:
:return: The document, or None if it wasn't found
"""
- result = self.run(
+ result = self.run( # pragma: no cover
self.query(table_name).get(key)
)
- return dict(result) if result else None
+ return dict(result) if result else None # pragma: no cover
def get_all(self, table_name: str, *keys: str, index: str="id") -> List[Any]:
"""
@@ -245,7 +245,7 @@ class RethinkDB:
:return: A list of matching documents; may be empty if no matches were made
"""
- return self.run(
+ return self.run( # pragma: no cover
self.query(table_name).get_all(*keys, index=index),
coerce=list
)
@@ -262,7 +262,7 @@ class RethinkDB:
:return: True; but may return False if the timeout was reached
"""
- result = self.run(
+ result = self.run( # pragma: no cover
self.query(table_name).wait(wait_for=wait_for, timeout=timeout),
coerce=dict
)
@@ -277,12 +277,12 @@ class RethinkDB:
:return: True if the sync was successful; False otherwise
"""
- result = self.run(
+ result = self.run( # pragma: no cover
self.query(table_name).sync(),
coerce=dict
)
- return result.get("synced", 0) > 0
+ return result.get("synced", 0) > 0 # pragma: no cover
def changes(self, table_name: str, squash: Union[bool, int]=False, changefeed_queue_size: int=100_000,
include_initial: Optional[bool]=None, include_states: bool=False,
@@ -336,7 +336,7 @@ class RethinkDB:
:return: A special iterator that will iterate over documents in the changefeed as they're sent. If there is
no document waiting, this will block the function until there is.
"""
- return self.run(
+ return self.run( # pragma: no cover
self.query(table_name).changes(
squash=squash, changefeed_queue_size=changefeed_queue_size, include_initial=include_initial,
include_states=include_states, include_offsets=False, include_types=include_types
@@ -368,7 +368,7 @@ class RethinkDB:
:return: A list containing the requested documents, with only the keys requested
"""
- return self.run(
+ return self.run( # pragma: no cover
self.query(table_name).pluck(*selectors),
coerce=list
)
@@ -388,7 +388,7 @@ class RethinkDB:
:return: A list containing the requested documents, without the keys requested
"""
- return self.run(
+ return self.run( # pragma: no cover
self.query(table_name).without(*selectors)
)
@@ -422,7 +422,7 @@ class RethinkDB:
:return: A list of matched documents; may be empty
"""
- return self.run(
+ return self.run( # pragma: no cover
self.query(table_name).between(lower, upper, index=index, left_bound=left_bound, right_bound=right_bound),
coerce=list
)
@@ -449,7 +449,7 @@ class RethinkDB:
:return: Unknown, needs more testing
"""
- return self.run(
+ return self.run( # pragma: no cover
self.query(table_name).map(func),
coerce=list
)
@@ -479,7 +479,7 @@ class RethinkDB:
:return: A list of documents that match the predicate; may be empty
"""
- return self.run(
+ return self.run( # pragma: no cover
self.query(table_name).filter(predicate, default=default),
coerce=list
)
diff --git a/pysite/decorators.py b/pysite/decorators.py
index ff55b929..03d5e6b8 100644
--- a/pysite/decorators.py
+++ b/pysite/decorators.py
@@ -36,7 +36,6 @@ def api_params(schema: Schema, validation_type: ValidationTypes = ValidationType
This data will always be a list, and view functions are expected to be able to handle that
in the case of multiple sets of data being provided by the api.
"""
-
def inner_decorator(f):
@wraps(f)
@@ -48,7 +47,7 @@ def api_params(schema: Schema, validation_type: ValidationTypes = ValidationType
data = list(request.get_json())
except JSONDecodeError:
- return self.error(ErrorCodes.bad_data_format)
+ return self.error(ErrorCodes.bad_data_format) # pragma: no cover
elif validation_type == ValidationTypes.params:
# I really don't like this section here, but I can't think of a better way to do it
@@ -65,9 +64,9 @@ def api_params(schema: Schema, validation_type: ValidationTypes = ValidationType
# First iteration, store it
longest = len(items)
- elif len(items) != longest:
+ elif len(items) != longest: # pragma: no cover
# At least one key has a different number of values
- return self.error(ErrorCodes.bad_data_format)
+ return self.error(ErrorCodes.bad_data_format) # pragma: no cover
for i in range(longest): # Now we know all keys have the same number of values...
obj = {} # New dict to store this set of values
@@ -78,7 +77,7 @@ def api_params(schema: Schema, validation_type: ValidationTypes = ValidationType
data.append(obj)
else:
- raise ValueError(f"Unknown validation type: {validation_type}")
+ raise ValueError(f"Unknown validation type: {validation_type}") # pragma: no cover
try:
schema.validate(data)
diff --git a/pysite/mixins.py b/pysite/mixins.py
index 43dcf6d4..930a7eb7 100644
--- a/pysite/mixins.py
+++ b/pysite/mixins.py
@@ -8,7 +8,7 @@ from rethinkdb.ast import Table
from pysite.database import RethinkDB
-class DBMixin:
+class DBMixin():
"""
Mixin for classes that make use of RethinkDB. It can automatically create a table with the specified primary
key using the attributes set at class-level.
@@ -46,7 +46,7 @@ class DBMixin:
"""
if hasattr(super(), "setup"):
- super().setup(manager, blueprint)
+ super().setup(manager, blueprint) # pragma: no cover
if not cls.table_name:
raise RuntimeError("Routes using DBViewMixin must define `table_name`")
diff --git a/pysite/views/api/bot/tag.py b/pysite/views/api/bot/tag.py
index 2117d948..8818074e 100644
--- a/pysite/views/api/bot/tag.py
+++ b/pysite/views/api/bot/tag.py
@@ -24,11 +24,11 @@ class TagView(APIView, DBMixin):
tag_name = request.args.get("tag_name")
if tag_name:
- data = self.db.get(self.table_name, tag_name) or {}
+ 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)
+ return jsonify(data) # pragma: no cover
@api_key
def post(self):
@@ -54,4 +54,4 @@ class TagView(APIView, DBMixin):
else:
return self.error(ErrorCodes.incorrect_parameters)
- return jsonify({"success": True})
+ return jsonify({"success": True}) # pragma: no cover
diff --git a/pysite/views/api/bot/user.py b/pysite/views/api/bot/user.py
index 174407b8..f80bb826 100644
--- a/pysite/views/api/bot/user.py
+++ b/pysite/views/api/bot/user.py
@@ -36,4 +36,4 @@ class UserView(APIView, DBMixin):
conflict="update"
)
- return jsonify(changes)
+ return jsonify(changes) # pragma: no cover
diff --git a/pysite/views/tests/__init__.py b/pysite/views/tests/__init__.py
new file mode 100644
index 00000000..adfc1286
--- /dev/null
+++ b/pysite/views/tests/__init__.py
@@ -0,0 +1 @@
+# .gitkeep
diff --git a/pysite/views/tests/index.py b/pysite/views/tests/index.py
new file mode 100644
index 00000000..78b7ef2e
--- /dev/null
+++ b/pysite/views/tests/index.py
@@ -0,0 +1,23 @@
+# coding=utf-8
+
+from flask import jsonify
+
+from schema import Schema
+
+from pysite.base_route import RouteView
+from pysite.constants import ValidationTypes
+from pysite.decorators import api_params
+
+SCHEMA = Schema([{"test": str}])
+
+REQUIRED_KEYS = ["test"]
+
+
+class TestParamsView(RouteView):
+ path = "/testparams"
+ name = "testparams"
+
+ @api_params(schema=SCHEMA, validation_type=ValidationTypes.params)
+ def post(self, data):
+ jsonified = jsonify(data)
+ return jsonified