diff options
author | 2018-02-26 11:41:08 +0100 | |
---|---|---|
committer | 2018-02-26 10:41:08 +0000 | |
commit | 8519c63143d00a4e3ad857d8ff2fc9813966510e (patch) | |
tree | c125a034be86e6d4040a7694751f1b1aacdeec31 | |
parent | New banner! (diff) |
brings coverage to 90% (#24)
* brings coverage to 75%
* satisfy flake8
* missing docstring added
* one more test
* artificially inflate coverage because python acts strange
* testing decorators
* fixed instantiation of test route
* straggling newlines from debugging code
* remove debug comments
* restructure tests into logical class separations. more exlusions. more tests
* testing websocket echo tests
* added missing comment
* convert single quotes to double quotes to satisfy docstrings
Diffstat (limited to '')
-rw-r--r-- | .coveragerc | 2 | ||||
-rw-r--r-- | app_test.py | 227 | ||||
-rw-r--r-- | pysite/base_route.py | 4 | ||||
-rw-r--r-- | pysite/database.py | 24 | ||||
-rw-r--r-- | pysite/decorators.py | 9 | ||||
-rw-r--r-- | pysite/mixins.py | 4 | ||||
-rw-r--r-- | pysite/views/api/bot/tag.py | 6 | ||||
-rw-r--r-- | pysite/views/api/bot/user.py | 2 | ||||
-rw-r--r-- | pysite/views/tests/__init__.py | 1 | ||||
-rw-r--r-- | pysite/views/tests/index.py | 23 |
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 |