def test_blueprint_arguments_documents_error_response( self, app, schemas, openapi_version, error_code ): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') blp.ARGUMENTS_PARSER.DEFAULT_VALIDATION_STATUS = 400 kwargs = {} if error_code: kwargs['error_status_code'] = error_code @blp.route('/') @blp.arguments(schemas.DocSchema, **kwargs) def func(): """Dummy view func""" api.register_blueprint(blp) spec = api.spec.to_dict() error_code = error_code or 400 assert ( spec['paths']['/test/']['get']['responses'][str(error_code)] == build_ref(api.spec, 'response', http.HTTPStatus(error_code).name) ) assert http.HTTPStatus(error_code).name in get_responses(api.spec)
def test_pagination_item_count_missing(self, app, header_name): """If item_count was not set, pass and warn""" api = Api(app) class CustomBlueprint(Blueprint): PAGINATION_HEADER_FIELD_NAME = header_name blp = CustomBlueprint('test', __name__, url_prefix='/test') @blp.route('/') @blp.response() @blp.paginate() def func(pagination_parameters): # Here, we purposely forget to call set_item_count # pagination_parameters.item_count = 2 return [1, 2] api.register_blueprint(blp) client = app.test_client() with mock.patch.object(app.logger, 'warning') as mock_warning: response = client.get('/test/') assert response.status_code == 200 assert 'X-Pagination' not in response.headers if header_name is None: assert mock_warning.call_count == 0 else: assert mock_warning.call_count == 1 assert mock_warning.call_args == (( 'item_count not set in endpoint test.func', ), )
def test_pagination_custom_header_field_name(self, app, header_name): """Test PAGINATION_HEADER_FIELD_NAME overriding""" api = Api(app) class CustomBlueprint(Blueprint): PAGINATION_HEADER_FIELD_NAME = header_name blp = CustomBlueprint('test', __name__, url_prefix='/test') @blp.route('/') @blp.response() @blp.paginate() def func(pagination_parameters): pagination_parameters.item_count = 2 return [1, 2] api.register_blueprint(blp) client = app.test_client() response = client.get('/test/') assert response.status_code == 200 assert 'X-Pagination' not in response.headers if header_name is not None: assert response.headers[header_name] == ( '{"total": 2, "total_pages": 1, ' '"first_page": 1, "last_page": 1, "page": 1}') # Also check there is only one pagination header assert len(response.headers.getlist(header_name)) == 1
def test_api_extra_spec_kwargs_init_app_update_init(self, app): """Test empty APISpec kwargs passed in init_app update init kwargs""" api = Api(spec_kwargs={'basePath': '/v1', 'host': 'example.com'}) api.init_app(app, spec_kwargs={'basePath': '/v2'}) spec = api.spec.to_dict() assert spec['host'] == 'example.com' assert spec['basePath'] == '/v2'
def test_api_int_float_converter(self, app, params, nb_type, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') param, output = params @blp.route('/<{}{}:val>'.format(nb_type, param)) def test(val): pass api.register_blueprint(blp) spec = api.spec.to_dict() schema = { 'int': { 'type': 'integer' }, 'float': { 'type': 'number' }, }[nb_type] schema.update(output) parameter = {'in': 'path', 'name': 'val', 'required': True} if openapi_version == '2.0': parameter.update(schema) else: parameter['schema'] = schema assert spec['paths']['/test/{val}']['parameters'] == [parameter]
def test_pagination_parameters_and_query_string_args(self, app, schemas): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') @blp.route('/') @blp.arguments(schemas.QueryArgsSchema, location="query") @blp.response() @blp.paginate(Page) def func(query_args): assert query_args['arg1'] == 'Test' assert query_args['arg2'] == 12 return range(30) api.register_blueprint(blp) client = app.test_client() # Pagination params in query string: OK response = client.get('/test/', query_string={ 'page': 2, 'page_size': 20, 'arg1': 'Test', 'arg2': 12 }) assert response.json == list(range(20, 30))
def test_api_register_field_parameters(self, app, mapping): api = Api(app) class CustomField(ma.fields.Field): pass api.register_field(CustomField, *mapping) class Document(ma.Schema): field = CustomField() api.spec.components.schema('Document', schema=Document) if len(mapping) == 2: properties = {'field': {'type': 'custom string'}} # If mapping format is None, it does not appear in the spec if mapping[1] is not None: properties['field']['format'] = mapping[1] else: properties = {'field': {'type': 'integer'}} assert get_schemas(api.spec)['Document'] == { 'properties': properties, 'type': 'object' }
def test_pagination_parameters_and_query_string_args(self, app, schemas): api = Api(app) blp = Blueprint("test", __name__, url_prefix="/test") @blp.route("/") @blp.arguments(schemas.QueryArgsSchema, location="query") @blp.response(200) @blp.paginate(Page) def func(query_args): assert query_args["arg1"] == "Test" assert query_args["arg2"] == 12 return range(30) api.register_blueprint(blp) client = app.test_client() # Pagination params in query string: OK response = client.get( "/test/", query_string={ "page": 2, "page_size": 20, "arg1": "Test", "arg2": 12 }, ) assert response.json == list(range(20, 30))
def test_blueprint_doc_merged_after_prepare_doc(self, app): app.config['OPENAPI_VERSION'] = '3.0.2' api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') # This is a dummy example. In real-life, use 'example' parameter. doc_example = { 'content': {'application/json': {'example': {'test': 123}}}} class ItemSchema(ma.Schema): test = ma.fields.Int() @blp.route('/') class Resource(MethodView): @blp.doc(**{'requestBody': doc_example}) @blp.doc(**{'responses': {200: doc_example}}) @blp.arguments(ItemSchema) @blp.response(ItemSchema) def get(self): pass api.register_blueprint(blp) spec = api.spec.to_dict() get = spec['paths']['/test/']['get'] assert get['requestBody']['content']['application/json'][ 'example'] == {'test': 123} resp = get['responses']['200'] assert resp['content']['application/json']['example'] == {'test': 123} assert 'schema' in resp['content']['application/json']
def test_api_int_float_converter(self, app, params, nb_type, openapi_version): app.config["OPENAPI_VERSION"] = openapi_version api = Api(app) blp = Blueprint("test", "test", url_prefix="/test") param, output = params @blp.route(f"/<{nb_type}{param}:val>") def test(val): pass api.register_blueprint(blp) spec = api.spec.to_dict() schema = { "int": { "type": "integer" }, "float": { "type": "number" }, }[nb_type] schema.update(output) parameter = {"in": "path", "name": "val", "required": True} if openapi_version == "2.0": parameter.update(schema) else: parameter["schema"] = schema assert spec["paths"]["/test/{val}"]["parameters"] == [parameter]
def test_pagination_header_documentation(self, app, openapi_version): """Test pagination header is documented""" app.config["OPENAPI_VERSION"] = openapi_version api = Api(app) class CustomBlueprint(Blueprint): PAGINATION_HEADER_NAME = "X-Custom-Pagination-Header" blp = CustomBlueprint("test", __name__, url_prefix="/test") @blp.route("/") @blp.response(200) @blp.paginate() def func(pagination_parameters): """Dummy view func""" api.register_blueprint(blp) spec = api.spec.to_dict() get = spec["paths"]["/test/"]["get"] assert "PaginationMetadata" in get_schemas(api.spec) if openapi_version == "2.0": assert get["responses"]["200"]["headers"] == { "X-Custom-Pagination-Header": { "description": "Pagination metadata", "schema": { "$ref": "#/definitions/PaginationMetadata" }, } } else: assert get["responses"]["200"]["headers"] == { "X-Custom-Pagination-Header": { "$ref": "#/components/headers/PAGINATION" } }
def test_api_extra_spec_kwargs_init_app_update_init(self, app): """Test empty APISpec kwargs passed in init_app update init kwargs""" api = Api(spec_kwargs={"basePath": "/v1", "host": "example.com"}) api.init_app(app, spec_kwargs={"basePath": "/v2"}) spec = api.spec.to_dict() assert spec["host"] == "example.com" assert spec["basePath"] == "/v2"
def test_api_register_field_parameters(self, app, mapping): api = Api(app) class CustomField(ma.fields.Field): pass api.register_field(CustomField, *mapping) class Document(ma.Schema): field = CustomField() api.spec.components.schema("Document", schema=Document) if len(mapping) == 2: properties = {"field": {"type": "custom string"}} # If mapping format is None, it does not appear in the spec if mapping[1] is not None: properties["field"]["format"] = mapping[1] else: properties = {"field": {"type": "integer"}} assert get_schemas(api.spec)["Document"] == { "properties": properties, "type": "object", }
def test_api_register_converter_before_and_after_init( self, app, openapi_version): api = Api() blp = Blueprint('test', 'test', url_prefix='/test') class CustomConverter_1(BaseConverter): pass class CustomConverter_2(BaseConverter): pass app.url_map.converters['custom_str_1'] = CustomConverter_1 app.url_map.converters['custom_str_2'] = CustomConverter_2 api.register_converter(CustomConverter_1, 'custom string 1') api.init_app(app) api.register_converter(CustomConverter_2, 'custom string 2') @blp.route('/1/<custom_str_1:val>') def test_func_1(val): pass @blp.route('/2/<custom_str_2:val>') def test_func_2(val): pass api.register_blueprint(blp) spec = api.spec.to_dict() parameter_1 = spec['paths']['/test/1/{val}']['parameters'][0] parameter_2 = spec['paths']['/test/2/{val}']['parameters'][0] if 'openapi_version' == '2.0': assert parameter_1['type'] == 'custom string 1' assert parameter_2['type'] == 'custom string 2' else: assert parameter_1['schema']['type'] == 'custom string 1' assert parameter_2['schema']['type'] == 'custom string 2'
def test_pagination_item_count_missing(self, app, header_name): """If item_count was not set, pass and warn""" api = Api(app) class CustomBlueprint(Blueprint): PAGINATION_HEADER_FIELD_NAME = header_name blp = CustomBlueprint('test', __name__, url_prefix='/test') @blp.route('/') @blp.response() @blp.paginate() def func(pagination_parameters): # Here, we purposely forget to set item_count # pagination_parameters.item_count = 2 return [1, 2] api.register_blueprint(blp) client = app.test_client() with pytest.warns(None) as record: response = client.get('/test/') if header_name is None: assert not record else: assert len(record) == 1 assert record[0].category == UserWarning assert str(record[0].message) == ( 'item_count not set in endpoint test.func.') assert response.status_code == 200 assert 'X-Pagination' not in response.headers
def test_api_lazy_registers_pagination_header(self, app): """Test pagination header is registered""" api = Api(app) # Declare dummy header to ensure get_headers doesn't fail header_1 = {"description": "Header 1"} api.spec.components.header("Header_1", header_1) # No route registered -> parameter header not registered headers = get_headers(api.spec) assert headers == {"Header_1": header_1} # Register routes with pagination blp = Blueprint("test", "test", url_prefix="/test") @blp.route("/") @blp.response(200) @blp.paginate() def test_get(val): pass api.register_blueprint(blp) headers = get_headers(api.spec) assert headers["PAGINATION"] == { "description": "Pagination metadata", "schema": {"$ref": "#/components/schemas/PaginationMetadata"}, }
def test_pagination_parameters_not_in_query_string(self, app): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') @blp.route('/') @blp.response() @blp.paginate(Page) def func(): return range(30) api.register_blueprint(blp) client = app.test_client() # Pagination params in query string: OK response = client.get('/test/', query_string={ 'page': 2, 'page_size': 20 }) assert response.json == list(range(20, 30)) # Pagination params in another location are ignored response = client.get( '/test/', data=json.dumps({ 'page': 2, 'page_size': 20 }), ) assert response.json == list(range(0, 10))
def test_pagination_header_documentation(self, app): """Test pagination header is documented""" api = Api(app) class CustomBlueprint(Blueprint): PAGINATION_HEADER_FIELD_NAME = 'X-Custom-Pagination-Header' blp = CustomBlueprint('test', __name__, url_prefix='/test') @blp.route('/') @blp.response() @blp.paginate() def func(pagination_parameters): """Dummy view func""" api.register_blueprint(blp) spec = api.spec.to_dict() get = spec['paths']['/test/']['get'] assert 'PaginationMetadata' in get_schemas(api.spec) assert get['responses']['200']['headers'] == { 'X-Custom-Pagination-Header': { 'description': 'Pagination metadata', 'schema': { '$ref': '#/components/schemas/PaginationMetadata' }, } }
def test_blueprint_doc_function(self, app): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') client = app.test_client() @blp.route('/', methods=( 'PUT', 'PATCH', )) @blp.doc(summary='Dummy func', description='Do dummy stuff') def view_func(): return {'Value': 'OK'} api.register_blueprint(blp) spec = api.spec.to_dict() path = spec['paths']['/test/'] for method in ( 'put', 'patch', ): assert path[method]['summary'] == 'Dummy func' assert path[method]['description'] == 'Do dummy stuff' response = client.put('/test/') assert response.status_code == 200 assert response.json == {'Value': 'OK'}
def test_blueprint_alt_response_wrapper(self, app, schemas, openapi_version): """Check alt_response passes response transparently""" app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) api.spec.components.response('ClientErrorResponse') blp = Blueprint('test', 'test', url_prefix='/test') client = app.test_client() @blp.route('/') @blp.response(200, schemas.DocSchema) @blp.alt_response(400, "ClientErrorResponse") def func(): return {'item_id': 12} api.register_blueprint(blp) paths = api.spec.to_dict()['paths'] response_ref = build_ref(api.spec, 'response', 'ClientErrorResponse') assert paths['/test/']['get']['responses']['400'] == response_ref resp = client.get('test/') assert resp.json == {'item_id': 12}
def test_blueprint_etag_documents_responses( self, app, method, decorate, etag_disabled, ): app.config['ETAG_DISABLED'] = etag_disabled api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') if decorate: @blp.route('/', methods=[method]) @blp.etag def func(): pass else: @blp.route('/', methods=[method]) def func(): pass api.register_blueprint(blp) operation = api.spec.to_dict()['paths']['/test/'][method.lower()] responses = operation.get('responses', {}) if not decorate or etag_disabled: assert '304' not in responses assert '412' not in responses assert '428' not in responses else: assert ('304' in responses) == (method in ['GET', 'HEAD']) assert ('412' in responses) == ( method in ['PUT', 'PATCH', 'DELETE']) assert ('428' in responses) == ( method in ['PUT', 'PATCH', 'DELETE'])
def test_blueprint_response_examples(self, app, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') examples = { 'example 1': { 'summary': 'Example 1', 'value': { 'name': 'One' } }, 'example 2': { 'summary': 'Example 2', 'value': { 'name': 'Two' } }, } @blp.route('/') @blp.response(200, examples=examples) def func(): pass api.register_blueprint(blp) get = api.spec.to_dict()['paths']['/test/']['get'] assert get['responses']['200']['content']['application/json'][ 'examples'] == examples
def test_blueprint_multiple_alt_response(self, app, openapi_version, schemas): """Check multiple nested calls to alt_response""" app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') @blp.route('/') @blp.alt_response(400, schemas.ClientErrorSchema) @blp.alt_response(404, 'NotFoundErrorResponse') def func(): pass api.register_blueprint(blp) paths = api.spec.to_dict()['paths'] schema_ref = build_ref(api.spec, 'schema', 'ClientError') response_ref = build_ref(api.spec, 'response', 'NotFoundErrorResponse') response = paths['/test/']['get']['responses']['400'] if openapi_version == '2.0': assert response['schema'] == schema_ref else: assert (response['content']['application/json']['schema'] == schema_ref) assert paths['/test/']['get']['responses']['404'] == response_ref
def test_blueprint_arguments_examples(self, app, schemas, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') example = {'field': 12} examples = {'example 1': {'field': 12}, 'example 2': {'field': 42}} @blp.route('/example') @blp.arguments(schemas.DocSchema, example=example) def func_example(): """Dummy view func""" @blp.route('/examples') @blp.arguments(schemas.DocSchema, examples=examples) def func_examples(): """Dummy view func""" api.register_blueprint(blp) spec = api.spec.to_dict() get = spec['paths']['/test/example']['get'] assert (get['requestBody']['content']['application/json']['example'] == example) get = spec['paths']['/test/examples']['get'] assert (get['requestBody']['content']['application/json']['examples'] == examples)
def test_blueprint_route_parameters(self, app, openapi_version): """Check path parameters docs are merged with auto docs""" app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') @blp.route('/<int:item_id>', parameters=[ 'TestParameter', { 'name': 'item_id', 'in': 'path', 'description': 'Item ID' }, ]) def get(item_id): pass api.register_blueprint(blp) spec = api.spec.to_dict() params = spec['paths']['/test/{item_id}']['parameters'] assert len(params) == 2 assert params[0] == build_ref(api.spec, 'parameter', 'TestParameter') assert params[1]['description'] == 'Item ID' if openapi_version == '2.0': assert params[1]['type'] == 'integer' else: assert params[1]['schema']['type'] == 'integer'
def test_blueprint_multiple_routes_per_view(self, app, as_method_view): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') if as_method_view: # Blueprint.route ensures a different endpoint is used for each # route. Otherwise, this would break in Blueprint.route when # calling as_view for the second time with the same endpoint. @blp.route('/route_1') @blp.route('/route_2') class Resource(MethodView): def get(self): pass else: @blp.route('/route_1') @blp.route('/route_2') def func(): pass api.register_blueprint(blp) paths = api.spec.to_dict()['paths'] assert 'get' in paths['/test/route_1'] assert 'get' in paths['/test/route_2']
def test_blueprint_enforce_method_order(self, app, http_methods): api = Api(app) class MyBlueprint(Blueprint): HTTP_METHODS = http_methods blp = MyBlueprint('test', __name__, url_prefix='/test') @blp.route('/') class Resource(MethodView): def post(self): pass def put(self): pass def options(self): pass def patch(self): pass def head(self): pass def delete(self): pass def get(self): pass api.register_blueprint(blp) methods = list(api.spec.to_dict()['paths']['/test/'].keys()) assert methods == [m.lower() for m in http_methods]
def test_blueprint_arguments_content_type(self, app, schemas, location, content_type, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') content_type = content_type or REQUEST_BODY_CONTENT_TYPE[location] @blp.route('/') @blp.arguments(schemas.DocSchema, location=location, content_type=content_type) def func(): """Dummy view func""" api.register_blueprint(blp) spec = api.spec.to_dict() get = spec['paths']['/test/']['get'] if openapi_version == '3.0.2': assert len(get['requestBody']['content']) == 1 assert content_type in get['requestBody']['content'] else: if content_type != 'application/json': assert get['consumes'] == [content_type] else: assert 'consumes' not in get
def test_blueprint_response_status_code_cast_to_string( self, app, status_code): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') # This is a dummy example. In real-life, use 'description' parameter. doc_desc = {'description': 'Description'} class ItemSchema(ma.Schema): test = ma.fields.Int() @blp.route('/') class Resource(MethodView): # When documenting a response, @blp.doc MUST use the same type # to express the status code as the one used in @blp.response. # (Default is 200 expressed as int.) @blp.doc(**{'responses': {status_code: doc_desc}}) @blp.arguments(ItemSchema) @blp.response(status_code, ItemSchema) def get(self): pass api.register_blueprint(blp) spec = api.spec.to_dict() resp = spec['paths']['/test/']['get']['responses']['200'] assert resp['description'] == 'Description' assert 'schema' in resp['content']['application/json']
def create_app(test_config=None): # create and configure the app app = Flask(__name__, instance_relative_config=True) app.config.from_object("lsgraph.config") if test_config is None: # load the instance config, if it exists, when not testing app.config.from_pyfile("config.py", silent=True) else: # load the test config if passed in app.config.from_mapping(test_config) if app.config["ENV"] == "production": assert app.config[ "SECRET_KEY"] != "DEVELOPMENT", "Secret key must be changed" # ensure the instance folder exists try: os.makedirs(app.instance_path) except OSError: pass from . import models models.db.init_app(app) migrate = Migrate(app, models.db) ext_celery.init_app(app) api = Api( app, spec_kwargs={ "security": [{ "APIKey": [], "AuthToken": [] }], "servers": [{ "url": "https://www.learnershape.com", "description": "Learnershape hosted service", }], }, ) api.spec.components.security_scheme("APIKey", { "type": "apiKey", "in": "header", "name": "X-API-Key" }) api.spec.components.security_scheme("AuthToken", { "type": "apiKey", "in": "header", "name": "X-Auth-Token" }) from . import api_v1 api.register_blueprint(api_v1.api) return app