def test_unit__exception_handler__error__error_content_malformed(self): class MyException(Exception): pass class MyErrorBuilder(MarshmallowDefaultErrorBuilder): def build_from_exception( self, exception: Exception, include_traceback: bool = False ) -> dict: # this is not matching with DefaultErrorBuilder schema return {} context = AgnosticContext(app=None) error_builder = MyErrorBuilder() wrapper = ExceptionHandlerControllerWrapper( MyException, context, error_builder=error_builder, processor_factory=lambda schema_: MarshmallowProcessor(error_builder.get_schema()), ) def raise_it(): raise MyException() wrapper = wrapper.get_wrapper(raise_it) with pytest.raises(OutputValidationException): wrapper()
async def test_unit__handle_exception_with_default_error_builder__ok__serpyco( self, test_client): from hapic.error.serpyco import SerpycoDefaultErrorBuilder from hapic.processor.serpyco import SerpycoProcessor app = AgnosticApp() hapic = Hapic() hapic.set_processor_class(SerpycoProcessor) hapic.set_context( AgnosticContext( app, default_error_builder=SerpycoDefaultErrorBuilder())) @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_
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_func__schema_in_doc__ok__many_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) @hapic.with_api_doc() @hapic.input_body(MySchema(many=True)) def my_controller(): return {"name": "test"} app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("definitions", {}).get("MySchema", {}) schema_def = doc.get("definitions", {}).get("MySchema", {}) assert schema_def.get("properties", {}).get("name", {}).get("type") 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.get("name") == "body" assert schema_ref["schema"] == { "items": { "$ref": "#/definitions/MySchema" }, "type": "array", }
def test_func__schema_with_many__ok__with_exclude(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): first_name = marshmallow.fields.String(required=True) last_name = marshmallow.fields.String(required=False) @hapic.with_api_doc() @hapic.output_body(MySchema(many=True, exclude=("last_name", ))) def my_controller(hapic_data=None): pass app.route("/", method="GET", callback=my_controller) doc = hapic.generate_doc() assert { "MySchema_without_last_name": { "type": "object", "properties": { "first_name": { "type": "string" } }, "required": ["first_name"], } } == doc["definitions"]
def test_unit__input_files__ok__file_is_not_present(self): hapic = Hapic(processor_class=MarshmallowProcessor) hapic.set_context( AgnosticContext( app=None, files_parameters={ # No file here }, ) ) 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)
def test_func__errors__http_status_as_int_description(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MyException(Exception): pass @hapic.with_api_doc() @hapic.handle_exception(MyException, http_code=400) def my_controller(hapic_data=None): assert hapic_data 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 "responses" in doc["paths"]["/upload"]["post"] assert "400" in doc["paths"]["/upload"]["post"]["responses"] assert { "description": "400", "schema": { "$ref": "#/definitions/DefaultErrorSchema" }, } == doc["paths"]["/upload"]["post"]["responses"]["400"]
def test_func_schema_in_doc__ok__additionals_fields__file(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.Raw(required=True, description="a description", example="00010") @hapic.with_api_doc() @hapic.input_files(MySchema()) def my_controller(): return 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 doc.get("paths").get("/upload").get("post").get("parameters")[0] field = doc.get("paths").get("/upload").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["in"] == "formData" assert field["type"] == "file" assert field["required"] is True
def test_func__input_files_doc__ok__one_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) @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"]
def test_unit__base_controller_wrapper__ok__no_behaviour(self): context = AgnosticContext(app=None) processor = MyProcessor() wrapper = InputOutputControllerWrapper(context, lambda: processor) @wrapper.get_wrapper def func(foo): return foo result = func(42) assert result == 42
def test_func__errors__multiple_same_http_status_description(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MyFirstException(Exception): pass class MySecondException(Exception): "Just a docstring" class MyThirdException(Exception): "Docstring not used" pass class MyFourthException(Exception): pass @hapic.with_api_doc() @hapic.handle_exception(MyFirstException, http_code=HTTPStatus.BAD_REQUEST) @hapic.handle_exception(MySecondException, http_code=HTTPStatus.BAD_REQUEST) @hapic.handle_exception(MyThirdException, http_code=HTTPStatus.BAD_REQUEST, description="explicit description") @hapic.handle_exception(MyFourthException, http_code=400) def my_controller(hapic_data=None): assert hapic_data 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 "responses" in doc["paths"]["/upload"]["post"] assert "400" in doc["paths"]["/upload"]["post"]["responses"] assert "description" assert doc["paths"]["/upload"]["post"]["responses"]["400"][ "description"] descriptions = doc["paths"]["/upload"]["post"]["responses"]["400"][ "description"].split("\n\n") assert "BAD_REQUEST: Bad request syntax or unsupported method" in descriptions assert "explicit description" in descriptions assert "400" in descriptions assert "Just a docstring" in descriptions assert "Docstring not used" not in descriptions assert doc["paths"]["/upload"]["post"]["responses"]["400"]["schema"] assert { "$ref": "#/definitions/DefaultErrorSchema" } == doc["paths"]["/upload"]["post"]["responses"]["400"]["schema"]
def test_unit__base_controller__ok__replaced_response(self): context = AgnosticContext(app=None) processor = MyProcessor() wrapper = MyControllerWrapper(context, lambda: processor) @wrapper.get_wrapper def func(foo): return foo # see MyControllerWrapper#before_wrapped_func result = func(666) # result have been replaced by MyControllerWrapper#before_wrapped_func assert {"error_response": "we are testing"} == result
def test_unit__controller_wrapper__ok__overload_input(self): context = AgnosticContext(app=None) processor = MyProcessor() wrapper = MyControllerWrapper(context, lambda: processor) @wrapper.get_wrapper def func(foo, added_parameter=None): # see MyControllerWrapper#before_wrapped_func assert added_parameter == "a value" return foo result = func(42) # See MyControllerWrapper#after_wrapped_function assert result == 84
def test_unit__output_data_wrapping__ok__nominal_case(self): context = AgnosticContext(app=None) processor = MyProcessor() wrapper = OutputControllerWrapper(context, lambda: processor) @wrapper.get_wrapper def func(foo, hapic_data=None): # If no use of input wrapper, no hapic_data is given assert not hapic_data return foo result = func(42) assert HTTPStatus.OK == result.status_code assert "43" == result.body
def test_unit__input_files__ok__file_is_present(self): hapic = Hapic(processor_class=MarshmallowProcessor) hapic.set_context(AgnosticContext(app=None, files_parameters={"file_abc": "10101010101"})) 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 "OK" == result
def test_unit__input_data_wrapping__ok__nominal_case(self): context = AgnosticContext(app=None, query_parameters=MultiDict((("foo", "bar"),))) processor = MyProcessor() wrapper = MyInputQueryControllerWrapper(context, lambda: processor) @wrapper.get_wrapper def func(foo, hapic_data=None): assert hapic_data assert isinstance(hapic_data, HapicData) # see MyControllerWrapper#before_wrapped_func assert hapic_data.query == {"foo": "bar"} return foo result = func(42) assert result == 42
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 test_unit__multi_query_param_values__ok__without_as_list(self): context = AgnosticContext( app=None, query_parameters=MultiDict((("user_id", "abc"), ("user_id", "def"))) ) processor = MySimpleProcessor() wrapper = InputQueryControllerWrapper(context, lambda: processor) @wrapper.get_wrapper def func(hapic_data=None): assert hapic_data assert isinstance(hapic_data, HapicData) # see MyControllerWrapper#before_wrapped_func assert "abc" == hapic_data.query.get("user_id") return hapic_data.query.get("user_id") result = func() assert result == "abc"
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__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"]
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_unit__output_data_wrapping__fail__error_response(self): context = AgnosticContext(app=None) processor = MarshmallowProcessor() processor.set_schema(MySchema()) wrapper = OutputControllerWrapper(context, lambda: processor) @wrapper.get_wrapper def func(foo): return "wrong result format" result = func(42) assert HTTPStatus.INTERNAL_SERVER_ERROR == result.status_code assert { "original_error": { "details": {"name": ["Missing data for required field."]}, "message": "Validation error of output data", }, "http_code": 500, } == json.loads(result.body)
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 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 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_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_
def test_func__errors__nominal_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) @hapic.with_api_doc() @hapic.handle_exception() def my_controller(hapic_data=None): assert hapic_data 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 "responses" in doc["paths"]["/upload"]["post"] assert "500" in doc["paths"]["/upload"]["post"]["responses"] assert { "description": Exception.__doc__, "schema": { "$ref": "#/definitions/DefaultErrorSchema" }, } == doc["paths"]["/upload"]["post"]["responses"]["500"]
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"]