async 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() async 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 = await 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', ), )
async 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() async def func(pagination_parameters): pagination_parameters.item_count = 2 return [1, 2] api.register_blueprint(blp) client = app.test_client() response = await 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}')
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_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_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_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', 'format': 'int32'}} assert get_schemas(api.spec)['Document'] == { 'properties': properties, 'type': 'object' }
async 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 = await client.put('/test/') assert response.status_code == 200 assert await response.json == {'Value': 'OK'}
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(ItemSchema, code=status_code) 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 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(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_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_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_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
async def test_blueprint_arguments_files_multipart(self, app, schemas, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') client = app.test_client() class MultipartSchema(ma.Schema): file_1 = Upload() file_2 = Upload() @blp.route('/', methods=['POST']) @blp.arguments(MultipartSchema, location='files') def func(files): return { 'file_1': files['file_1'].read().decode(), 'file_2': files['file_2'].read().decode(), } api.register_blueprint(blp) spec = api.spec.to_dict() files = { 'file_1': (io.BytesIO('Test 1'.encode()), 'file_1.txt'), 'file_2': (io.BytesIO('Test 2'.encode()), 'file_2.txt'), } response = await client.post('/test/', data=files) assert response.json == {'file_1': 'Test 1', 'file_2': 'Test 2'} if openapi_version == '2.0': for param in spec['paths']['/test/']['post']['parameters']: assert param['in'] == 'formData' assert param['type'] == 'file' else: assert ( spec['paths']['/test/']['post']['requestBody']['content'] == { 'multipart/form-data': { 'schema': { '$ref': '#/components/schemas/Multipart' } } }) assert (spec['components']['schemas']['Multipart'] == { 'type': 'object', 'properties': { 'file_1': { 'type': 'string', 'format': 'binary' }, 'file_2': { 'type': 'string', 'format': 'binary' }, } })
def test_blueprint_arguments_required(self, app, schemas, required, location_map, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') location, _ = location_map if required is None: @blp.route('/') @blp.arguments(schemas.DocSchema, location=location) def func(): pass else: @blp.route('/') @blp.arguments(schemas.DocSchema, required=required, location=location) def func(): pass api.register_blueprint(blp) get = api.spec.to_dict()['paths']['/test/']['get'] # OAS3 / json, form, files if (openapi_version == '3.0.2' and location in REQUEST_BODY_CONTENT_TYPE): # Body parameter in 'requestBody' assert 'requestBody' in get # Check required defaults to True assert get['requestBody']['required'] == (required is not False) # OAS2 / json elif location == 'json': parameters = get['parameters'] # One parameter: the schema assert len(parameters) == 1 assert 'schema' in parameters[0] assert 'requestBody' not in get # Check required defaults to True assert parameters[0]['required'] == (required is not False) # OAS2-3 / all else: parameters = get['parameters'] # One parameter: the 'field' field in DocSchema assert len(parameters) == 1 assert parameters[0]['name'] == 'field' assert 'requestBody' not in get # Check the required parameter has no impact. # Only the required attribute of the field matters assert parameters[0]['required'] is False
def app_fixture(request, collection, schemas, app): """Return an app client for each configuration - pagination in function / post-pagination - function / method view - default / custom pagination parameters """ blp_factory, as_method_view, custom_params = request.param blueprint = blp_factory(collection, schemas, as_method_view, custom_params) api = Api(app) api.register_blueprint(blueprint) return namedtuple('AppFixture', ('client', 'custom_params'))(app.test_client(), custom_params)
def test_api_openapi_version_parameters(self, app): """Test OpenAPI version must be passed, as app param or spec kwarg""" app.config['OPENAPI_VERSION'] = '3.0.2' api = Api(app) assert api.spec.to_dict()['openapi'] == '3.0.2' del app.config['OPENAPI_VERSION'] api = Api(app, spec_kwargs={'openapi_version': '3.0.2'}) assert api.spec.to_dict()['openapi'] == '3.0.2' with pytest.raises(OpenAPIVersionNotSpecified, match='The OpenAPI version must be specified'): Api(app)
def test_api_register_field_before_and_after_init(self, app, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api() class CustomField_1(ma.fields.Field): pass class CustomField_2(ma.fields.Field): pass api.register_field(CustomField_1, 'custom string', 'custom') api.init_app(app) api.register_field(CustomField_2, 'custom string', 'custom') class Schema_1(ma.Schema): int_1 = ma.fields.Int() custom_1 = CustomField_1() class Schema_2(ma.Schema): int_2 = ma.fields.Int() custom_2 = CustomField_2() api.spec.components.schema('Schema_1', schema=Schema_1) api.spec.components.schema('Schema_2', schema=Schema_2) schema_defs = get_schemas(api.spec) assert schema_defs['Schema_1']['properties']['custom_1'] == { 'type': 'custom string', 'format': 'custom' } assert schema_defs['Schema_2']['properties']['custom_2'] == { 'type': 'custom string', 'format': 'custom' }
def test_blueprint_doc_called_twice(self, app): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') @blp.route('/') @blp.doc(summary='Dummy func') @blp.doc(description='Do dummy stuff') def view_func(): pass api.register_blueprint(blp) spec = api.spec.to_dict() path = spec['paths']['/test/'] assert path['get']['summary'] == 'Dummy func' assert path['get']['description'] == 'Do dummy stuff'
def test_blueprint_response_headers(self, app): api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') headers = {'X-Header': {'description': 'Custom header'}} @blp.route('/') @blp.response(headers=headers) def func(): pass api.register_blueprint(blp) get = api.spec.to_dict()['paths']['/test/']['get'] assert get['responses']['200']['headers'] == headers
def test_blueprint_pagination(self, app, schemas, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') @blp.route('/') @blp.arguments(schemas.QueryArgsSchema, location='query') @blp.paginate() def func(): """Dummy view func""" api.register_blueprint(blp) spec = api.spec.to_dict() # Check parameters are documented parameters = spec['paths']['/test/']['get']['parameters'] # Page assert parameters[0]['name'] == 'page' assert parameters[0]['in'] == 'query' assert parameters[0]['required'] is False if openapi_version == '2.0': assert parameters[0]['type'] == 'integer' assert parameters[0]['default'] == 1 assert parameters[0]['minimum'] == 1 else: assert parameters[0]['schema']['type'] == 'integer' assert parameters[0]['schema']['default'] == 1 assert parameters[0]['schema']['minimum'] == 1 # Page size assert parameters[1]['name'] == 'page_size' assert parameters[1]['in'] == 'query' assert parameters[1]['required'] is False if openapi_version == '2.0': assert parameters[1]['type'] == 'integer' assert parameters[1]['default'] == 10 assert parameters[1]['minimum'] == 1 assert parameters[1]['maximum'] == 100 else: assert parameters[1]['schema']['type'] == 'integer' assert parameters[1]['schema']['default'] == 10 assert parameters[1]['schema']['minimum'] == 1 assert parameters[1]['schema']['maximum'] == 100 # Other query string parameters assert parameters[1]['in'] == 'query' assert parameters[2]['name'] == 'arg1' assert parameters[2]['in'] == 'query' assert parameters[3]['name'] == 'arg2' assert parameters[3]['in'] == 'query'
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 if openapi_version == '2.0': assert params == [ build_ref(api.spec, 'parameter', 'TestParameter'), { 'name': 'item_id', 'in': 'path', 'required': True, 'description': 'Item ID', 'format': 'int32', 'type': 'integer' }, ] else: assert params == [ build_ref(api.spec, 'parameter', 'TestParameter'), { 'name': 'item_id', 'in': 'path', 'required': True, 'description': 'Item ID', 'schema': { 'format': 'int32', 'type': 'integer' } }, ]
async def test_blueprint_arguments_multiple(self, app, schemas, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') client = app.test_client() @blp.route('/', methods=('POST', )) @blp.arguments(schemas.DocSchema) @blp.arguments(schemas.QueryArgsSchema, location='query') async def func(document, query_args): return {'document': document, 'query_args': query_args} api.register_blueprint(blp) spec = api.spec.to_dict() # Check parameters are documented parameters = spec['paths']['/test/']['post']['parameters'] assert parameters[0]['name'] == 'arg1' assert parameters[0]['in'] == 'query' assert parameters[1]['name'] == 'arg2' assert parameters[1]['in'] == 'query' if openapi_version == '2.0': assert len(parameters) == 3 assert parameters[2]['in'] == 'body' assert 'schema' in parameters[2] else: assert len(parameters) == 2 assert 'schema' in spec['paths']['/test/']['post']['requestBody'][ 'content']['application/json'] # Check parameters are passed as arguments to view function item_data = {'field': 12} response = await client.post( '/test/', data=json.dumps(item_data), headers={"Content-Type": "application/json"}, query_string={'arg1': 'test'}) assert response.status_code == 200 assert await response.json == { 'document': { 'db_field': 12 }, 'query_args': { 'arg1': 'test' }, }
async def test_apipec_serve_redoc_using_cdn(self, app, redoc_version): class NewAppConfig(AppConfig): OPENAPI_URL_PREFIX = 'api-docs' OPENAPI_REDOC_PATH = 'redoc' if redoc_version: OPENAPI_REDOC_VERSION = redoc_version app.config.from_object(NewAppConfig) Api(app) client = app.test_client() response_redoc = await client.get('/api-docs/redoc') assert (response_redoc.headers['Content-Type'] == 'text/html; charset=utf-8') redoc_version = redoc_version or 'latest' if redoc_version == 'latest' or redoc_version.startswith('v1'): redoc_url = ('https://rebilly.github.io/ReDoc/releases/' '{}/redoc.min.js'.format(redoc_version)) else: redoc_url = ( 'https://cdn.jsdelivr.net/npm/redoc@' '{}/bundles/redoc.standalone.js'.format(redoc_version)) script_elem = '<script src="{}"></script>'.format(redoc_url) response_data: bytes = await response_redoc.get_data(raw=True) assert script_elem in response_data.decode()
async def test_apipec_serve_spec_empty_path(self, app, prefix, path, tested): """Test empty string or (equivalently) single slash as paths Documentation can be served at root of application. """ class NewAppConfig(AppConfig): OPENAPI_URL_PREFIX = prefix OPENAPI_SWAGGER_UI_VERSION = '3.0.2' mapping = { 'json': 'OPENAPI_JSON_PATH', 'redoc': 'OPENAPI_REDOC_PATH', 'swagger-ui': 'OPENAPI_SWAGGER_UI_PATH', } setattr(NewAppConfig, mapping[tested], path) app.config.from_object(NewAppConfig) Api(app) client = app.test_client() if tested == 'json': response_json_docs = await client.get('/') else: response_json_docs = await client.get('//openapi.json') response_doc_page = await client.get('/') assert response_doc_page.status_code == 200 assert (response_doc_page.headers['Content-Type'] == 'text/html; charset=utf-8') response_json = await response_json_docs.json assert response_json['info'] == {'version': '1', 'title': 'API Test'}
async def test_blueprint_response_response_object(self, app, schemas): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') client = app.test_client() @blp.route('/response') # Schema is ignored when response object is returned @blp.response(schemas.DocSchema, code=200) def func_response(): return {}, 201, {'X-header': 'test'} api.register_blueprint(blp) response = await client.get('/test/response') assert response.status_code == 201 assert response.status == '201 CREATED' assert response.json == {} assert response.headers['X-header'] == 'test'
async def test_etag_response_object(self, app): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') client = app.test_client() @blp.route('/') @blp.etag @blp.response() async def func_response_etag(): # When the view function returns a Response object, # the ETag must be specified manually blp.set_etag('test') return jsonify({}) api.register_blueprint(blp) response = await client.get('/test/') assert await response.json == {} assert response.get_etag() == (blp._generate_etag('test'), False)
def test_api_extra_spec_plugins(self, app, schemas, openapi_version): """Test extra plugins can be passed to internal APISpec instance""" app.config['OPENAPI_VERSION'] = openapi_version class MyPlugin(apispec.BasePlugin): def schema_helper(self, name, definition, **kwargs): return {'dummy': 'whatever'} api = Api(app, spec_kwargs={'extra_plugins': (MyPlugin(), )}) api.spec.components.schema('Pet', schema=schemas.DocSchema) assert get_schemas(api.spec)['Pet']['dummy'] == 'whatever'
def test_api_apispec_sets_base_path(self, app, openapi_version, base_path): app.config['OPENAPI_VERSION'] = openapi_version if base_path is not None: app.config['APPLICATION_ROOT'] = base_path api = Api(app) spec = api.spec.to_dict() if openapi_version == '2.0': assert spec['basePath'] == base_path or '/' else: assert 'basePath' not in spec
def test_api_gets_apispec_parameters_from_app(self, app, openapi_version): app.config['API_VERSION'] = 'v42' app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) spec = api.spec.to_dict() assert spec['info'] == {'title': 'API Test', 'version': 'v42'} if openapi_version == '2.0': assert spec['swagger'] == '2.0' else: assert spec['openapi'] == '3.0.2'
def test_blueprint_response_description(self, app): api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') @blp.route('/route_1') @blp.response() def func_1(): pass @blp.route('/route_2') @blp.response(description='Test') def func_2(): pass api.register_blueprint(blp) get_1 = api.spec.to_dict()['paths']['/test/route_1']['get'] assert 'description' not in get_1['responses']['200'] get_2 = api.spec.to_dict()['paths']['/test/route_2']['get'] assert get_2['responses']['200']['description'] == 'Test'