-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #155 from algoo/fix/154_framework_agnostic_tests
Fix/154 framework agnostic tests
- Loading branch information
Showing
12 changed files
with
467 additions
and
308 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
# flask regular expression to locate url parameters | ||
from http import HTTPStatus | ||
import json | ||
import re | ||
import typing | ||
|
||
from multidict import MultiDict | ||
|
||
from hapic.context import BaseContext | ||
from hapic.context import HandledException | ||
from hapic.context import RouteRepresentation | ||
from hapic.decorator import DECORATION_ATTRIBUTE_NAME, DecoratedController | ||
from hapic.error.marshmallow import MarshmallowDefaultErrorBuilder | ||
from hapic.exception import RouteNotFound | ||
from hapic.processor.main import ProcessValidationError | ||
from hapic.processor.main import RequestParameters | ||
|
||
PATH_URL_REGEX = re.compile(r"<([^:<>]+)(?::[^<>]+)?>") | ||
|
||
|
||
class AgnosticApp(object): | ||
""" | ||
Framework Agnostic App for AgnosticContext. Cannot | ||
be run as a true wsgi app. | ||
""" | ||
def __init__(self): | ||
self.routes = [] # type: typing.List[RouteRepresentation] | ||
|
||
def route(self, rule: str, method: str, callback: typing.Callable): | ||
self.routes.append(RouteRepresentation(rule, method, callback)) | ||
|
||
|
||
class AgnosticResponse(object): | ||
def __init__(self, response, http_code, mimetype): | ||
self.response = response | ||
self.http_code = http_code | ||
self.mimetype = mimetype | ||
|
||
@property | ||
def status_code(self): | ||
return self.http_code | ||
|
||
@property | ||
def body(self): | ||
return self.response | ||
|
||
|
||
class AgnosticContext(BaseContext): | ||
""" | ||
Agnostic Context, doesn't need any web framework. | ||
This handle: | ||
- Documentation | ||
- View-Based-Exception | ||
This does not handle: | ||
- Context-Based-Exception | ||
""" | ||
|
||
def __init__( | ||
self, | ||
app=AgnosticApp(), | ||
default_error_builder=MarshmallowDefaultErrorBuilder(), | ||
path_parameters=None, | ||
query_parameters=None, | ||
body_parameters=None, | ||
form_parameters=None, | ||
header_parameters=None, | ||
files_parameters=None, | ||
debug=False, | ||
path_url_regex=PATH_URL_REGEX, | ||
) -> None: | ||
super().__init__(default_error_builder=default_error_builder) | ||
self.debug = debug | ||
self._handled_exceptions = ( | ||
[] | ||
) # type: typing.List[HandledException] # nopep8 | ||
self.app = app | ||
self._exceptions_handler_installed = False | ||
self.path_url_regex = path_url_regex | ||
self.path_parameters = path_parameters or {} | ||
self.query_parameters = query_parameters or MultiDict() | ||
self.body_parameters = body_parameters or {} | ||
self.form_parameters = form_parameters or MultiDict() | ||
self.header_parameters = header_parameters or {} | ||
self.files_parameters = files_parameters or {} | ||
|
||
def get_request_parameters(self, *args, **kwargs) -> RequestParameters: | ||
return RequestParameters( | ||
path_parameters=self.path_parameters, | ||
query_parameters=self.query_parameters, | ||
body_parameters=self.body_parameters, | ||
form_parameters=self.form_parameters, | ||
header_parameters=self.header_parameters, | ||
files_parameters=self.files_parameters, | ||
) | ||
|
||
def get_validation_error_response( | ||
self, | ||
error: ProcessValidationError, | ||
http_code: HTTPStatus = HTTPStatus.BAD_REQUEST, | ||
) -> typing.Any: | ||
return self.get_response( | ||
response=json.dumps( | ||
{ | ||
"original_error": { | ||
"details": error.details, | ||
"message": error.message, | ||
}, | ||
"http_code": http_code, | ||
} | ||
), | ||
http_code=http_code, | ||
) | ||
|
||
def _add_exception_class_to_catch( | ||
self, exception_class: typing.Type[Exception], http_code: int | ||
) -> None: | ||
self._handled_exceptions.append( | ||
HandledException(exception_class, http_code) | ||
) | ||
|
||
def _get_handled_exception_class_and_http_codes( | ||
self, | ||
) -> typing.List[HandledException]: | ||
return self._handled_exceptions | ||
|
||
def find_route(self, decorated_controller: "DecoratedController"): | ||
reference = decorated_controller.reference | ||
for route in self.app.routes: | ||
route_token = getattr( | ||
route.original_route_object, DECORATION_ATTRIBUTE_NAME, None | ||
) | ||
|
||
match_with_wrapper = ( | ||
route.original_route_object == reference.wrapper | ||
) | ||
match_with_wrapped = ( | ||
route.original_route_object == reference.wrapped | ||
) | ||
match_with_token = route_token == reference.token | ||
|
||
if match_with_wrapper or match_with_wrapped or match_with_token: | ||
return RouteRepresentation( | ||
rule=self.get_swagger_path(route.rule), | ||
method=route.method.lower(), | ||
original_route_object=route, | ||
) | ||
# TODO BS 20171010: Raise exception or print error ? see #10 | ||
raise RouteNotFound( | ||
'Decorated route "{}" was not found in bottle routes'.format( | ||
decorated_controller.name | ||
) | ||
) | ||
|
||
def get_swagger_path(self, contextualised_rule: str) -> str: | ||
return self.path_url_regex.sub(r"{\1}", contextualised_rule) | ||
|
||
def by_pass_output_wrapping(self, response: typing.Any) -> bool: | ||
if isinstance(response, AgnosticResponse): | ||
return True | ||
return False | ||
|
||
def get_response( | ||
self, | ||
# TODO BS 20171228: rename into response_content | ||
response: str, | ||
http_code: int, | ||
mimetype: str = "application/json", | ||
): | ||
return AgnosticResponse(response, http_code, mimetype) | ||
|
||
def is_debug(self) -> bool: | ||
return self.debug |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
# coding: utf-8 | ||
import pytest | ||
from webtest import TestApp | ||
|
||
from hapic import Hapic | ||
from hapic import MarshmallowProcessor | ||
from hapic.error.marshmallow import MarshmallowDefaultErrorBuilder | ||
from hapic.ext.aiohttp.context import AiohttpContext | ||
from hapic.ext.bottle import BottleContext | ||
from hapic.ext.flask import FlaskContext | ||
from hapic.ext.pyramid import PyramidContext | ||
from tests.base import Base | ||
|
||
|
||
class TestContextExceptionHandling(Base): | ||
""" | ||
Test apply exception on all context with all different frameworks supported | ||
""" | ||
|
||
@pytest.mark.asyncio | ||
async def test_func__catch_one_exception__ok__aiohttp_case(self, test_client): | ||
from aiohttp import web | ||
app = web.Application() | ||
hapic = Hapic(processor_class=MarshmallowProcessor) | ||
context = AiohttpContext(app=app) | ||
hapic.set_context(context) | ||
|
||
async def my_view(request): | ||
raise ZeroDivisionError("An exception message") | ||
|
||
app.add_routes([ | ||
web.get('/my-view', my_view), | ||
]) | ||
# FIXME - G.M - 17-05-2018 - Verify if: | ||
# - other view that work/raise an other exception do not go | ||
# into this code for handle this exceptions. | ||
# - response come clearly from there, not from web framework: | ||
# Check not only http code, but also body. | ||
# see issue #158 (https://github.com/algoo/hapic/issues/158) | ||
context.handle_exception(ZeroDivisionError, http_code=400) | ||
test_app = await test_client(app) | ||
response = await test_app.get("/my-view") | ||
|
||
assert 400 == response.status | ||
|
||
def test_func__catch_one_exception__ok__flask_case(self): | ||
from flask import Flask | ||
app = Flask(__name__) | ||
hapic = Hapic(processor_class=MarshmallowProcessor) | ||
context = FlaskContext(app=app) | ||
hapic.set_context(context) | ||
|
||
def my_view(): | ||
raise ZeroDivisionError("An exception message") | ||
|
||
app.add_url_rule( | ||
'/my-view', | ||
view_func=my_view, | ||
methods=['GET'] | ||
) | ||
# FIXME - G.M - 17-05-2018 - Verify if: | ||
# - other view that work/raise an other exception do not go | ||
# into this code for handle this exceptions. | ||
# - response come clearly from there, not from web framework: | ||
# Check not only http code, but also body. | ||
# see issue #158 (https://github.com/algoo/hapic/issues/158) | ||
context.handle_exception(ZeroDivisionError, http_code=400) | ||
|
||
test_app = TestApp(app) | ||
response = test_app.get("/my-view", status="*") | ||
|
||
assert 400 == response.status_code | ||
|
||
def test_func__catch_one_exception__ok__bottle_case(self): | ||
import bottle | ||
app = bottle.Bottle() | ||
hapic = Hapic(processor_class=MarshmallowProcessor) | ||
context = BottleContext(app=app) | ||
hapic.set_context(context) | ||
|
||
def my_view(): | ||
raise ZeroDivisionError("An exception message") | ||
|
||
app.route("/my-view", method="GET", callback=my_view) | ||
# FIXME - G.M - 17-05-2018 - Verify if: | ||
# - other view that work/raise an other exception do not go | ||
# into this code for handle this exceptions. | ||
# - response come clearly from there, not from web framework: | ||
# Check not only http code, but also body. | ||
# see issue #158 (https://github.com/algoo/hapic/issues/158) | ||
context.handle_exception(ZeroDivisionError, http_code=400) | ||
|
||
test_app = TestApp(app) | ||
response = test_app.get("/my-view", status="*") | ||
|
||
assert 400 == response.status_code | ||
|
||
def test_func__catch_one_exception__ok__pyramid(self): | ||
from pyramid.config import Configurator | ||
configurator = Configurator(autocommit=True) | ||
context = PyramidContext( | ||
configurator, | ||
default_error_builder=MarshmallowDefaultErrorBuilder(), | ||
) | ||
hapic = Hapic(processor_class=MarshmallowProcessor) | ||
hapic.set_context(context) | ||
|
||
def my_view(context, request): | ||
raise ZeroDivisionError("An exception message") | ||
|
||
configurator.add_route("my_view", "/my-view", request_method="GET") | ||
configurator.add_view(my_view, route_name="my_view", renderer="json") | ||
|
||
# FIXME - G.M - 17-05-2018 - Verify if: | ||
# - other view that work/raise an other exception do not go | ||
# into this code for handle this exceptions. | ||
# - response come clearly from there, not from web framework: | ||
# Check not only http code, but also body. | ||
# see issue #158 (https://github.com/algoo/hapic/issues/158) | ||
context.handle_exception(ZeroDivisionError, http_code=400) | ||
|
||
app = configurator.make_wsgi_app() | ||
test_app = TestApp(app) | ||
response = test_app.get("/my-view", status="*") | ||
|
||
assert 400 == response.status_code |
Oops, something went wrong.