def test_unit__exception_handled__ok__exception_error_dict(self): class MyException(Exception): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.error_dict = {} context = AgnosticContext(app=None) error_builder = MarshmallowDefaultErrorBuilder() wrapper = ExceptionHandlerControllerWrapper( MyException, context, error_builder=error_builder, http_code=HTTPStatus.INTERNAL_SERVER_ERROR, processor_factory=lambda schema_: MarshmallowProcessor(error_builder.get_schema()), ) @wrapper.get_wrapper def func(foo): exc = MyException("We are testing") exc.error_detail = {"foo": "bar"} raise exc response = func(42) assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR assert { "message": "We are testing", "details": {"error_detail": {"foo": "bar"}}, "code": None, } == json.loads(response.body)
def test_unit__map_binding__ok__mapped_method(self): hapic_ = hapic.Hapic(processor_class=MarshmallowProcessor) app = bottle.Bottle() context = hapic.ext.bottle.BottleContext( app=app, default_error_builder=MarshmallowDefaultErrorBuilder() ) hapic_.set_context(context) class MyControllers(object): def bind(self, app): app.route("/", callback=self.controller_a) @hapic_.with_api_doc() def controller_a(self): pass my_controllers = MyControllers() my_controllers.bind(app) assert hapic_.controllers decoration = hapic_.controllers[0] route = context.find_route(decoration) assert route # Important note: instance controller_a method is # not class controller_a, so no matches with callbacks assert route.original_route_object.callback != MyControllers.controller_a assert route.original_route_object.callback != decoration.reference.wrapped assert route.original_route_object.callback != decoration.reference.wrapper
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
def test_unit__generate_output_stream_doc__ok__nominal_case( self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class OuputStreamItemSchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) @hapic.with_api_doc() @hapic.output_stream(OuputStreamItemSchema()) async def get_users(request, hapic_data): pass app = web.Application(debug=True) app.router.add_get("/", get_users) hapic.set_context( AiohttpContext( app, default_error_builder=MarshmallowDefaultErrorBuilder())) doc = hapic.generate_doc("aiohttp", "testing") # INFO BS 2019-04-15: Prevent keep of OrderedDict doc = json.loads(json.dumps(doc)) assert "/" in doc.get("paths") assert "get" in doc["paths"]["/"] assert "200" in doc["paths"]["/"]["get"].get("responses", {}) assert { "items": { "$ref": "#/definitions/OuputStreamItemSchema" }, "type": "array" } == doc["paths"]["/"]["get"]["responses"]["200"]["schema"]
async def test_aiohttp_input_path__error_wrong_input_parameter( self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class InputPathSchema(marshmallow.Schema): i = marshmallow.fields.Integer() @hapic.input_path(InputPathSchema()) async def hello(request, hapic_data: HapicData): i = hapic_data.path.get("i") return web.Response(text="integer: {}".format(str(i))) app = web.Application(debug=True) app.router.add_get("/{i}", hello) hapic.set_context( AiohttpContext( app, default_error_builder=MarshmallowDefaultErrorBuilder())) client = await aiohttp_client(app) resp = await client.get("/bob") # NOTE: should be integer here assert resp.status == 400 error = await resp.json() assert "Validation error of input data" in error.get("message") assert {"i": ["Not a valid integer."]} == error.get("details")
async def test_aiohttp_output_body__error__incorrect_output_body( self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class OuputBodySchema(marshmallow.Schema): i = marshmallow.fields.Integer(required=True) @hapic.output_body(OuputBodySchema()) async def hello(request): return {"i": "bob"} # NOTE: should be integer app = web.Application(debug=True) app.router.add_get("/", hello) hapic.set_context( AiohttpContext( app, default_error_builder=MarshmallowDefaultErrorBuilder())) client = await aiohttp_client(app) resp = await client.get("/") assert resp.status == 500 data = await resp.text() data = await resp.json() assert "Validation error of output data" == data.get("message") assert { "i": ["Missing data for required field."] } == data.get("details")
async def test_unit__post_file__ok__put_success(self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class InputFilesSchema(marshmallow.Schema): avatar = marshmallow.fields.Raw() @hapic.with_api_doc() @hapic.input_files(InputFilesSchema()) async def update_avatar(request: Request, hapic_data: HapicData): avatar = hapic_data.files["avatar"] assert isinstance(avatar, FileField) assert b"text content of file" == avatar.file.read() return Response(body="ok") app = web.Application(debug=True) app.router.add_put("/avatar", update_avatar) hapic.set_context( AiohttpContext( app, default_error_builder=MarshmallowDefaultErrorBuilder())) client = await aiohttp_client(app) resp = await client.put( "/avatar", data={"avatar": io.StringIO("text content of file")}) assert resp.status == 200
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] 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 {}
async def test_aiohttp_output_stream__ok__nominal_case(self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class AsyncGenerator: def __init__(self): self._iterator = iter([{"name": "Hello, bob"}, {"name": "Hello, franck"}]) async def __aiter__(self): return self async def __anext__(self): return next(self._iterator) class OuputStreamItemSchema(marshmallow.Schema): name = marshmallow.fields.String() @hapic.output_stream(OuputStreamItemSchema()) async def hello(request): return AsyncGenerator() app = web.Application(debug=True) app.router.add_get("/", hello) hapic.set_context( AiohttpContext(app, default_error_builder=MarshmallowDefaultErrorBuilder()) ) client = await aiohttp_client(app) resp = await client.get("/") assert resp.status == 200 line = await resp.content.readline() assert b'{"name": "Hello, bob"}\n' == line line = await resp.content.readline() assert b'{"name": "Hello, franck"}\n' == line
async def test_unit__post_file__ok__missing_file(self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class InputFilesSchema(marshmallow.Schema): avatar = marshmallow.fields.Raw(required=True) @hapic.with_api_doc() @hapic.input_files(InputFilesSchema()) async def update_avatar(request: Request, hapic_data: HapicData): raise AssertionError("Test should no pass here") app = web.Application(debug=True) app.router.add_put("/avatar", update_avatar) hapic.set_context( AiohttpContext( app, default_error_builder=MarshmallowDefaultErrorBuilder())) client = await aiohttp_client(app) resp = await client.put("/avatar") assert resp.status == 400 json_ = await resp.json() assert { "details": { "avatar": ["Missing data for required field"] }, "message": "Validation error of input data", "code": None, } == json_
async def test_aiohttp_output_stream__error__interrupt_py37( self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class OuputStreamItemSchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) from .py37.stream import get_func_with_output_stream_and_error hello = get_func_with_output_stream_and_error(hapic, OuputStreamItemSchema, ignore_on_error=False) app = web.Application(debug=True) app.router.add_get("/", hello) hapic.set_context( AiohttpContext( app, default_error_builder=MarshmallowDefaultErrorBuilder())) client = await aiohttp_client(app) resp = await client.get("/") assert resp.status == 200 line = await resp.content.readline() assert b'{"name": "Hello, bob"}\n' == line line = await resp.content.readline() assert b"" == line
async def test_unit__general_exception_handling__ok__nominal_case( self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) @hapic.with_api_doc() async def hello(request): return 1 / 0 app = web.Application(debug=True) app.router.add_get("/", hello) context = AiohttpContext( app, default_error_builder=MarshmallowDefaultErrorBuilder()) context.handle_exception(ZeroDivisionError, 400) hapic.set_context(context) client = await aiohttp_client(app) resp = await client.get("/") json = await resp.json() assert resp.status == 400 assert { "details": { "error_detail": {} }, "message": "division by zero", "code": None, } == json
async def test_aiohttp_input_body__error__incorrect_input_body( self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class InputBodySchema(marshmallow.Schema): i = marshmallow.fields.Integer() @hapic.input_body(InputBodySchema()) async def hello(request, hapic_data: HapicData): i = hapic_data.body.get("i") return web.Response(text="integer, {}".format(i)) app = web.Application(debug=True) app.router.add_post("/", hello) hapic.set_context( AiohttpContext( app, default_error_builder=MarshmallowDefaultErrorBuilder())) client = await aiohttp_client(app) resp = await client.post("/", data={"i": "bob"}) # NOTE: should be int assert resp.status == 400 error = await resp.json() assert "Validation error of input data" in error.get("message") assert {"i": ["Not a valid integer."]} == error.get("details")
def build_from_exception(self, exception: Exception, include_traceback: bool = False) -> dict: error_dict = MarshmallowDefaultErrorBuilder.build_from_exception( self, exception, include_traceback) code = getattr(exception, "error_code", None) error_dict["code"] = code return error_dict
def test_unit__generate_doc__ok__nominal_case(self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class InputPathSchema(marshmallow.Schema): username = marshmallow.fields.String(required=True) class InputQuerySchema(marshmallow.Schema): show_deleted = marshmallow.fields.Boolean(required=False) class UserSchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) @hapic.with_api_doc() @hapic.input_path(InputPathSchema()) @hapic.input_query(InputQuerySchema()) @hapic.output_body(UserSchema()) async def get_user(request, hapic_data): pass @hapic.with_api_doc() @hapic.input_path(InputPathSchema()) @hapic.output_body(UserSchema()) async def post_user(request, hapic_data): pass app = web.Application(debug=True) app.router.add_get("/{username}", get_user) app.router.add_post("/{username}", post_user) hapic.set_context( AiohttpContext(app, default_error_builder=MarshmallowDefaultErrorBuilder()) ) doc = hapic.generate_doc("aiohttp", "testing") # INFO BS 2019-04-15: Prevent keep of OrderedDict doc = json.loads(json.dumps(doc)) assert "UserSchema" in doc.get("definitions") assert {"name": {"type": "string"}} == doc["definitions"]["UserSchema"].get("properties") assert "/{username}" in doc.get("paths") assert "get" in doc["paths"]["/{username}"] assert "post" in doc["paths"]["/{username}"] assert [ {"name": "username", "in": "path", "required": True, "type": "string"}, {"name": "show_deleted", "in": "query", "required": False, "type": "boolean"}, ] == doc["paths"]["/{username}"]["get"]["parameters"] assert { "200": {"schema": {"$ref": "#/definitions/UserSchema"}, "description": "200"} } == doc["paths"]["/{username}"]["get"]["responses"] assert [{"name": "username", "in": "path", "required": True, "type": "string"}] == doc[ "paths" ]["/{username}"]["post"]["parameters"] assert { "200": {"schema": {"$ref": "#/definitions/UserSchema"}, "description": "200"} } == doc["paths"]["/{username}"]["get"]["responses"]
def get_flask_context(): from example.fake_api.flask_api import hapic as h flask_app = Flask(__name__) controllers = FlaskController() controllers.bind(flask_app) h.reset_context() h.set_context( FlaskContext(flask_app, default_error_builder=MarshmallowDefaultErrorBuilder())) return {"hapic": h, "app": flask_app}
def get_bottle_context(): from example.fake_api.bottle_api import hapic as h bottle_app = Bottle() h.reset_context() h.set_context( BottleContext(bottle_app, default_error_builder=MarshmallowDefaultErrorBuilder())) controllers = BottleController() controllers.bind(bottle_app) return {"hapic": h, "app": bottle_app}
def get_pyramid_context(): from example.fake_api.pyramid_api import hapic as h configurator = Configurator(autocommit=True) controllers = PyramidController() controllers.bind(configurator) h.reset_context() h.set_context( PyramidContext(configurator, default_error_builder=MarshmallowDefaultErrorBuilder())) pyramid_app = configurator.make_wsgi_app() return {"hapic": h, "app": pyramid_app}
def test_unit__exception_handled__ok__nominal_case(self): context = AgnosticContext(app=None) error_builder = MarshmallowDefaultErrorBuilder() wrapper = ExceptionHandlerControllerWrapper( ZeroDivisionError, context, error_builder=error_builder, http_code=HTTPStatus.INTERNAL_SERVER_ERROR, processor_factory=lambda schema_: MarshmallowProcessor(error_builder.get_schema()), ) @wrapper.get_wrapper def func(foo): raise ZeroDivisionError("We are testing") response = func(42) assert HTTPStatus.INTERNAL_SERVER_ERROR == response.status_code assert { "details": {"error_detail": {}}, "message": "We are testing", "code": None, } == json.loads(response.body)
def test_unit__general_exception_handling__ok__nominal_case(self): hapic_ = hapic.Hapic(processor_class=MarshmallowProcessor) app = bottle.Bottle() context = BottleContext(app=app, default_error_builder=MarshmallowDefaultErrorBuilder()) hapic_.set_context(context) def my_view(): raise ZeroDivisionError("An exception message") app.route("/my-view", method="GET", callback=my_view) context.handle_exception(ZeroDivisionError, http_code=400) test_app = TestApp(app) response = test_app.get("/my-view", status="*") assert 400 == response.status_code
def get_flask_context(): h = Hapic(processor_class=MarshmallowProcessor) flask_app = Flask(__name__) h.reset_context() h.set_context(FlaskContext(flask_app, default_error_builder=MarshmallowDefaultErrorBuilder())) class MySchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) @h.with_api_doc() @h.input_body(MySchema()) def my_controller(): return {"name": "test"} flask_app.add_url_rule("/test", view_func=my_controller, methods=["POST"]) return {"hapic": h, "app": flask_app}
def get_bottle_context(): h = Hapic(processor_class=MarshmallowProcessor) bottle_app = Bottle() h.reset_context() h.set_context(BottleContext(bottle_app, default_error_builder=MarshmallowDefaultErrorBuilder())) class MySchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) @h.with_api_doc() @h.input_body(MySchema()) def my_controller(): return {"name": "test"} bottle_app.route("/test", method="POST", callback=my_controller) return {"hapic": h, "app": bottle_app}
def test_unit__generate_doc_with_wildcard__ok__fixed_methods(self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) @hapic.with_api_doc() async def a_proxy(request): pass app = web.Application(debug=True) app.router.add_route(hdrs.METH_ANY, path="/", handler=a_proxy) hapic.set_context( AiohttpContext(app, default_error_builder=MarshmallowDefaultErrorBuilder()) ) doc = hapic.generate_doc("aiohttp", "testing", wildcard_method_replacement=["head"]) # INFO BS 2019-04-15: Prevent keep of OrderedDict doc = json.loads(json.dumps(doc)) assert 1 == len(doc["paths"]["/"]) assert "head" in doc["paths"]["/"]
async def test_aiohttp_handle_excpetion__ok__nominal_case(self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) @hapic.handle_exception(ZeroDivisionError, http_code=400) async def hello(request): 1 / 0 app = web.Application(debug=True) app.router.add_get("/", hello) hapic.set_context( AiohttpContext(app, default_error_builder=MarshmallowDefaultErrorBuilder()) ) client = await aiohttp_client(app) resp = await client.get("/") assert resp.status == 400 data = await resp.json() assert "division by zero" == data.get("message")
def get_aiohttp_context(): h = Hapic(async_=True, processor_class=MarshmallowProcessor) aiohttp_app = web.Application(debug=True) h.reset_context() h.set_context( AiohttpContext(aiohttp_app, default_error_builder=MarshmallowDefaultErrorBuilder()) ) class MySchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) @h.with_api_doc() @h.input_body(MySchema()) def my_controller(): return {"name": "test"} aiohttp_app.router.add_post("/", my_controller) return {"hapic": h, "app": aiohttp_app}
def test_unit__map_binding__ok__decorated_function(self): hapic_ = hapic.Hapic(processor_class=MarshmallowProcessor) app = bottle.Bottle() context = hapic.ext.bottle.BottleContext( app=app, default_error_builder=MarshmallowDefaultErrorBuilder() ) hapic_.set_context(context) @hapic_.with_api_doc() @app.route("/") def controller_a(): pass assert hapic_.controllers decoration = hapic_.controllers[0] route = context.find_route(decoration) assert route assert route.original_route_object.callback != controller_a assert route.original_route_object.callback == decoration.reference.wrapped assert route.original_route_object.callback != decoration.reference.wrapper
async def test_aiohttp_output_body__ok__nominal_case(self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class OuputBodySchema(marshmallow.Schema): name = marshmallow.fields.String() @hapic.output_body(OuputBodySchema()) async def hello(request): return {"name": "bob"} app = web.Application(debug=True) app.router.add_get("/", hello) hapic.set_context( AiohttpContext(app, default_error_builder=MarshmallowDefaultErrorBuilder()) ) client = await aiohttp_client(app) resp = await client.get("/") assert resp.status == 200 data = await resp.json() assert "bob" == data.get("name")
def get_pyramid_context(): h = Hapic(processor_class=MarshmallowProcessor) configurator = Configurator(autocommit=True) h.reset_context() h.set_context( PyramidContext(configurator, default_error_builder=MarshmallowDefaultErrorBuilder()) ) class MySchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) @h.with_api_doc() @h.input_body(MySchema()) def my_controller(): return {"name": "test"} configurator.add_route("test", "/test", request_method="POST") configurator.add_view(my_controller, route_name="test") pyramid_app = configurator.make_wsgi_app() return {"hapic": h, "app": pyramid_app}
async def test_aiohttp_input_body__ok_nominal_case(self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class InputBodySchema(marshmallow.Schema): name = marshmallow.fields.String() @hapic.input_body(InputBodySchema()) async def hello(request, hapic_data: HapicData): name = hapic_data.body.get("name") return web.Response(text="Hello, {}".format(name)) app = web.Application(debug=True) app.router.add_post("/", hello) hapic.set_context( AiohttpContext(app, default_error_builder=MarshmallowDefaultErrorBuilder()) ) client = await aiohttp_client(app) resp = await client.post("/", data={"name": "bob"}) assert resp.status == 200 text = await resp.text() assert "Hello, bob" in text
def test_unit__handle_exception_with_default_error_builder__ok__marshmallow( self): app = AgnosticApp() hapic = Hapic() hapic.set_processor_class(MarshmallowProcessor) hapic.set_context( AgnosticContext( app, default_error_builder=MarshmallowDefaultErrorBuilder())) @hapic.with_api_doc() @hapic.handle_exception(ZeroDivisionError, http_code=400) def my_view(): 1 / 0 response = my_view() json_ = json.loads(response.body) assert { "code": None, "details": { "error_detail": {} }, "message": "division by zero", } == json_