Skip to content

Commit

Permalink
Merge pull request #155 from algoo/fix/154_framework_agnostic_tests
Browse files Browse the repository at this point in the history
Fix/154 framework agnostic tests
  • Loading branch information
inkhey authored Mar 26, 2019
2 parents c60ac86 + 0593064 commit 7f3523e
Show file tree
Hide file tree
Showing 12 changed files with 467 additions and 308 deletions.
Empty file added hapic/ext/agnostic/__init__.py
Empty file.
172 changes: 172 additions & 0 deletions hapic/ext/agnostic/context.py
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
80 changes: 3 additions & 77 deletions tests/base.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
# -*- coding: utf-8 -*-
import json
import typing
import sys

from multidict import MultiDict

from hapic.context import HandledException
from hapic.error.marshmallow import MarshmallowDefaultErrorBuilder
from hapic.ext.bottle import BottleContext
from hapic.processor.main import ProcessValidationError
from hapic.processor.main import RequestParameters
import pytest

try: # Python 3.5+
from http import HTTPStatus
Expand All @@ -19,71 +12,4 @@
class Base(object):
pass


# TODO BS 20171105: Make this bottle agnostic !
class MyContext(BottleContext):
def __init__(
self,
app,
fake_path_parameters=None,
fake_query_parameters=None,
fake_body_parameters=None,
fake_form_parameters=None,
fake_header_parameters=None,
fake_files_parameters=None,
) -> None:
super().__init__(
app=app, default_error_builder=MarshmallowDefaultErrorBuilder()
)
self._handled_exceptions = (
[]
) # type: typing.List[HandledException] # nopep8
self._exceptions_handler_installed = False
self.fake_path_parameters = fake_path_parameters or {}
self.fake_query_parameters = fake_query_parameters or MultiDict()
self.fake_body_parameters = fake_body_parameters or {}
self.fake_form_parameters = fake_form_parameters or MultiDict()
self.fake_header_parameters = fake_header_parameters or {}
self.fake_files_parameters = fake_files_parameters or {}

def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
return RequestParameters(
path_parameters=self.fake_path_parameters,
query_parameters=self.fake_query_parameters,
body_parameters=self.fake_body_parameters,
form_parameters=self.fake_form_parameters,
header_parameters=self.fake_header_parameters,
files_parameters=self.fake_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:
if not self._exceptions_handler_installed:
self._install_exceptions_handler()
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
serpyco_compatible_python = pytest.mark.skipif(sys.version_info < (3, 6), reason="serpyco dataclasses required python>3.6")
1 change: 0 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
[
"func/fake_api/test_fake_api_aiohttp_serpyco.py",
"func/test_doc_serpyco.py",
"func/test_serpyco_errors.py",
"unit/test_serpyco_processor.py",
]
)
126 changes: 126 additions & 0 deletions tests/func/test_context_exception_handling.py
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
Loading

0 comments on commit 7f3523e

Please sign in to comment.