Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/154 framework agnostic tests #155

Merged
merged 5 commits into from
Mar 26, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
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.
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.
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):
# TODO - G.M - 17-05-2018 - Move/refactor this test
# in order to have here only framework agnostic test
# and framework_specific
# test somewhere else.
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.
inkhey marked this conversation as resolved.
Show resolved Hide resolved
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