async def test_unit__global_exception__ok__nominal_case(self, aiohttp_client): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) @hapic.with_api_doc() async def divide_by_zero(request): raise ZeroDivisionError() app = web.Application(debug=True) context = AiohttpContext(app) hapic.set_context(context) context.handle_exception(ZeroDivisionError, http_code=HTTPStatus.BAD_REQUEST) app.router.add_get("/", divide_by_zero) client = await aiohttp_client(app) response = await client.get("/") assert 400 == response.status
def test_func_schema_in_doc__ok__additionals_fields__forms__string(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.String( required=True, description="a description", example="00010", format="binary", enum=["01000", "11111"], maxLength=5, minLength=5, # Theses none string specific parameters should disappear # in query/path maximum=400, # exclusiveMaximun=False, # minimum=0, # exclusiveMinimum=True, # multipleOf=1, ) @hapic.with_api_doc() @hapic.input_forms(MySchema()) def my_controller(): return app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() assert "multipart/form-data" in doc["paths"]["/paper"]["post"][ "consumes"] assert doc.get("paths").get("/paper").get("post").get("parameters")[0] field = doc.get("paths").get("/paper").get("post").get("parameters")[0] assert field[ "description"] == "a description\n\n*example value: 00010*" # INFO - G.M - 01-06-2018 - Field example not allowed here, # added in description instead assert "example" not in field assert field["format"] == "binary" assert field["in"] == "formData" assert field["type"] == "string" assert field["maxLength"] == 5 assert field["minLength"] == 5 assert field["required"] is True assert field["enum"] == ["01000", "11111"] assert "maximum" not in field
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 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}
async def test_aiohttp_output_stream__error__interrupt( self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class AsyncGenerator: def __init__(self): self._iterator = iter([ { "name": "Hello, bob" }, { "nameZ": "Hello, Z" }, # This line is incorrect { "name": "Hello, franck" }, # This line must not be reached ]) async def __aiter__(self): return self async def __anext__(self): return next(self._iterator) class OuputStreamItemSchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) @hapic.output_stream(OuputStreamItemSchema(), ignore_on_error=False) 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"" == line
def test_func__output_file_doc__ok__nominal_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) @hapic.with_api_doc() @hapic.output_file(["image/jpeg"]) def my_controller(): return b"101010100101" app.route("/avatar", method="GET", callback=my_controller) doc = hapic.generate_doc() assert doc assert "/avatar" in doc["paths"] assert "produces" in doc["paths"]["/avatar"]["get"] assert "image/jpeg" in doc["paths"]["/avatar"]["get"]["produces"] assert "200" in doc["paths"]["/avatar"]["get"]["responses"]
def test_func__output_file_doc__ok__nominal_case(self): hapic = Hapic() hapic.set_context(MyContext()) app = bottle.Bottle() @hapic.with_api_doc() @hapic.output_file('image/jpeg') def my_controller(): return b'101010100101' app.route('/avatar', method='GET', callback=my_controller) doc = hapic.generate_doc(app) assert doc assert '/avatar' in doc['paths'] assert 'produce' in doc['paths']['/avatar']['get'] assert 'image/jpeg' in doc['paths']['/avatar']['get']['produce'] assert 200 in doc['paths']['/avatar']['get']['responses']
def test_func__tags__ok__nominal_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) @hapic.with_api_doc(tags=["foo", "bar"]) def my_controller(hapic_data=None): assert hapic_data assert hapic_data.files app.route("/upload", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("paths") assert "/upload" in doc["paths"] assert "post" in doc["paths"]["/upload"] assert "tags" in doc["paths"]["/upload"]["post"] assert ["foo", "bar"] == doc["paths"]["/upload"]["post"]["tags"]
async def test_unit__general_exception_handling__ok__exception_list( self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) @hapic.with_api_doc() async def zero(request): return 1 / 0 @hapic.with_api_doc() async def key(request): return dict()["foo"] app = web.Application(debug=True) app.router.add_get("/a", zero) app.router.add_get("/b", key) context = AiohttpContext( app, default_error_builder=MarshmallowDefaultErrorBuilder()) context.handle_exceptions([ZeroDivisionError, KeyError], 400) hapic.set_context(context) client = await aiohttp_client(app) resp = await client.get("/a") json = await resp.json() assert resp.status == 400 assert { "details": { "error_detail": {} }, "message": "division by zero", "code": None, } == json resp = await client.get("/a") json = await resp.json() assert resp.status == 400 assert { "details": { "error_detail": {} }, "message": "division by zero", "code": None, } == json
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 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_request_header__ok__lowercase_key(self, aiohttp_client): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class HeadersSchema(marshmallow.Schema): foo = marshmallow.fields.String(required=True) @hapic.with_api_doc() @hapic.input_headers(HeadersSchema()) async def hello(request, hapic_data: HapicData): return web.json_response(hapic_data.headers) app = web.Application(debug=True) hapic.set_context(AiohttpContext(app)) app.router.add_get("/", hello) client = await aiohttp_client(app) response = await client.get("/", headers={"FOO": "bar"}) assert 200 == response.status json_ = await response.json() assert {"foo": "bar"} == json_
def test_unit__input_files__ok__file_is_present(self): hapic = Hapic() hapic.set_context( MyContext(fake_files_parameters={ 'file_abc': '10101010101', })) class MySchema(marshmallow.Schema): file_abc = marshmallow.fields.Raw(required=True) @hapic.with_api_doc() @hapic.input_files(MySchema()) def my_controller(hapic_data=None): assert hapic_data assert hapic_data.files return 'OK' result = my_controller() assert 'OK' == result
def test_func_schema_in_doc__ok__additionals_fields__body__number(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.Integer( required=True, description="a number", example="12", format="int64", enum=[4, 6], # Theses none string specific parameters should disappear # in query/path maximum=14, exclusiveMaximun=False, minimum=0, exclusiveMinimum=True, multipleOf=2, ) @hapic.with_api_doc() @hapic.input_body(MySchema()) def my_controller(): return app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() schema_field = (doc.get("definitions", {}).get("MySchema", {}).get("properties", {}).get("category", {})) assert schema_field assert schema_field["description"] == "a number" assert schema_field["example"] == "12" assert schema_field["format"] == "int64" assert schema_field["type"] == "integer" assert schema_field["maximum"] == 14 assert schema_field["minimum"] == 0 assert schema_field["exclusiveMinimum"] is True assert schema_field["enum"] == [4, 6] assert schema_field["multipleOf"] == 2
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_func_schema_in_doc__ok__additionals_fields__path__number(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.Integer( required=True, description="a number", example="12", format="int64", enum=[4, 6], # Theses none string specific parameters should disappear # in query/path maximum=14, exclusiveMaximun=False, minimum=0, exclusiveMinimum=True, multipleOf=2, ) @hapic.with_api_doc() @hapic.input_path(MySchema()) def my_controller(): return app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("paths").get("/paper").get("post").get("parameters")[0] field = doc.get("paths").get("/paper").get("post").get("parameters")[0] assert field["description"] == "a number\n\n*example value: 12*" # INFO - G.M - 01-06-2018 - Field example not allowed here, # added in description instead assert "example" not in field assert field["format"] == "int64" assert field["in"] == "path" assert field["type"] == "integer" assert field["maximum"] == 14 assert field["minimum"] == 0 assert field["exclusiveMinimum"] is True assert field["required"] is True assert field["enum"] == [4, 6] assert field["multipleOf"] == 2
def test_func__docstring__ok__simple_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) @hapic.with_api_doc() def my_controller(hapic_data=None): """ Hello doc """ assert hapic_data assert hapic_data.files app.route("/upload", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("paths") assert "/upload" in doc["paths"] assert "post" in doc["paths"]["/upload"] assert "description" in doc["paths"]["/upload"]["post"] assert "Hello doc" == doc["paths"]["/upload"]["post"]["description"]
def test_func__schema_in_doc__ok__many_and_exclude_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) name2 = marshmallow.fields.String(required=True) @hapic.with_api_doc() @hapic.input_body(MySchema(exclude=("name2", ), many=True)) def my_controller(): return {"name": "test"} app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() definitions = doc.get("definitions", {}) # TODO - G-M - Find better way to find our new schema # Do Better test when we were able to set correctly schema name # according to content schema_name = None for elem in definitions.keys(): if elem != "MySchema": schema_name = elem break assert schema_name schema_def = definitions[schema_name] assert schema_def.get("properties", {}).get("name", {}).get("type") == "string" assert doc.get("paths").get("/paper").get("post").get("parameters")[0] schema_ref = doc.get("paths").get("/paper").get("post").get( "parameters")[0] assert schema_ref.get("in") == "body" assert schema_ref["schema"] == { "items": { "$ref": "#/definitions/{}".format(schema_name) }, "type": "array", }
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}
def test_unit__input_files__ok__file_is_empty_string(self): hapic = Hapic(processor_class=MarshmallowProcessor) hapic.set_context(AgnosticContext(app=None, files_parameters={"file_abc": ""})) class MySchema(marshmallow.Schema): file_abc = marshmallow.fields.Raw(required=True) @hapic.input_files(MySchema()) def my_controller(hapic_data=None): assert hapic_data assert hapic_data.files return "OK" result = my_controller() assert HTTPStatus.BAD_REQUEST == result.status_code assert { "http_code": 400, "original_error": { "details": {"file_abc": ["Missing data for required field"]}, "message": "Validation error of input data", }, } == json.loads(result.body)
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 test_unit__input_files__ok__file_is_empty_string(self): hapic = Hapic() hapic.set_context(MyContext(fake_files_parameters={ 'file_abc': '', })) class MySchema(marshmallow.Schema): file_abc = marshmallow.fields.Raw(required=True) @hapic.with_api_doc() @hapic.input_files(MySchema()) def my_controller(hapic_data=None): assert hapic_data assert hapic_data.files return 'OK' result = my_controller() assert 'http_code' in result assert HTTPStatus.BAD_REQUEST == result['http_code'] assert { 'file_abc': ['Missing data for required field'] } == result['original_error'].details
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
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 test_func__enum__nominal_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.String( validate=OneOf(["foo", "bar"])) @hapic.with_api_doc() @hapic.input_body(MySchema()) def my_controller(): return app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() assert ["foo", "bar"] == doc.get("definitions", {}).get("MySchema", {}).get("properties", {}).get("category", {}).get("enum")
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_
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__input_files_doc__ok__two_file(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): file_abc = marshmallow.fields.Raw(required=True) file_def = marshmallow.fields.Raw(required=False) @hapic.with_api_doc() @hapic.input_files(MySchema()) def my_controller(hapic_data=None): assert hapic_data assert hapic_data.files app.route("/upload", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc assert "/upload" in doc["paths"] assert "consumes" in doc["paths"]["/upload"]["post"] assert "multipart/form-data" in doc["paths"]["/upload"]["post"][ "consumes"] assert "parameters" in doc["paths"]["/upload"]["post"] assert { "name": "file_abc", "required": True, "in": "formData", "type": "file" } in doc["paths"]["/upload"]["post"]["parameters"] assert { "name": "file_def", "required": False, "in": "formData", "type": "file" } in doc["paths"]["/upload"]["post"]["parameters"]
def test_func_schema_in_doc__ok__additionals_fields__body__string(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.String( required=True, description="a description", example="00010", format="binary", enum=["01000", "11111"], maxLength=5, minLength=5, ) @hapic.with_api_doc() @hapic.input_body(MySchema()) def my_controller(): return app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() schema_field = (doc.get("definitions", {}).get("MySchema", {}).get("properties", {}).get("category", {})) assert schema_field assert schema_field["description"] == "a description" assert schema_field["example"] == "00010" assert schema_field["format"] == "binary" assert schema_field["type"] == "string" assert schema_field["maxLength"] == 5 assert schema_field["minLength"] == 5 assert schema_field["enum"] == ["01000", "11111"]
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