def test_blueprint_enforce_method_order(self, app): api = Api(app) blp = Blueprint('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_spec = api.spec.to_dict()['paths']['/test/'] assert list(methods_spec.keys()) == [m.lower() for m in HTTP_METHODS]
def test_args_parser_nested_query_arguments(self, app): api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') class UserNameSchema(ma.Schema): class Meta: strict = True first_name = ma.fields.String() last_name = ma.fields.String() class UserSchema(ma.Schema): class Meta: strict = True user = ma.fields.Nested(UserNameSchema) @blp.route('/') class TestMethod(MethodView): @blp.arguments(UserSchema, location='query') def get(self, args): return jsonify(args) api.register_blueprint(blp) res = app.test_client().get('/test/', query_string={ 'user.first_name': 'Chuck', 'user.last_name': 'Norris'}) assert res.json == { 'user': {'first_name': 'Chuck', 'last_name': 'Norris'}}
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}')
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_blueprint_route_path_parameter_default(self, app, as_method_view): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') if as_method_view: @blp.route('/<int:user_id>') @blp.route('/', defaults={'user_id': 1}) class Resource(MethodView): def get(self, user_id): pass else: @blp.route('/<int:user_id>') @blp.route('/', defaults={'user_id': 1}) def func(user_id): pass api.register_blueprint(blp) paths = api.spec.to_dict()['paths'] assert 'parameters' not in paths['/test/']['get'] assert paths['/test/{user_id}']['get']['parameters'][0][ 'name'] == 'user_id'
def test_api_register_converter( self, app, view_type, custom_format, openapi_version): api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') class CustomConverter(BaseConverter): pass app.url_map.converters['custom_str'] = CustomConverter api.register_converter(CustomConverter, 'custom string', custom_format) if view_type == 'function': @blp.route('/<custom_str:val>') def test_func(val): return jsonify(val) else: @blp.route('/<custom_str:val>') class TestMethod(MethodView): def get(self, val): return jsonify(val) api.register_blueprint(blp) spec = api.spec.to_dict() schema = {'type': 'custom string'} # If custom_format is None (default), it does not appear in the spec if custom_format is not None: schema['format'] = 'custom' parameter = {'in': 'path', 'name': 'val', 'required': True} if 'openapi_version' == '2.0': parameter.update(schema) else: parameter['schema'] = schema assert spec['paths']['/test/{val}']['get']['parameters'] == [parameter]
def test_apipec_path_response_schema_many(self, app, schemas): """Check that plural response is documented as array in the spec""" api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') @blp.route('/schema_many_false') @blp.response(schemas.DocSchema(many=False)) def many_false(): pass @blp.route('/schema_many_true') @blp.response(schemas.DocSchema(many=True)) def many_true(): pass api.register_blueprint(blp) paths = api.spec.to_dict()['paths'] schema_many_false = paths['/test/schema_many_false']['get'][ 'responses'][200]['schema'] assert schema_many_false['type'] == 'object' assert 'items' not in schema_many_false schema_many_true = paths['/test/schema_many_true']['get']['responses'][ 200]['schema'] assert schema_many_true['type'] == 'array' assert schema_many_true['items']['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_blueprint_arguments_location(self, app, schemas, location_map, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') location, openapi_location = location_map if location is not None: @blp.route('/') @blp.arguments(schemas.DocSchema, location=location) def func(): """Dummy view func""" else: @blp.route('/') @blp.arguments(schemas.DocSchema) def func(): """Dummy view func""" api.register_blueprint(blp) spec = api.spec.to_dict() get = spec['paths']['/test/']['get'] if openapi_location != 'body' or openapi_version == '2.0': loc = get['parameters'][0]['in'] assert loc == openapi_location assert 'requestBody' not in get else: # In OpenAPI v3, 'body' parameter is in 'requestBody' assert 'parameters' not in get assert 'requestBody' in get
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_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_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_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_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_blueprint_doc_info_from_docstring(self, app): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') @blp.route('/') class Resource(MethodView): def get(self): """Docstring get summary""" def put(self): """Docstring put summary Docstring put description """ @blp.doc(summary='Decorator patch summary', description='Decorator patch description') def patch(self): """Docstring patch summary Docstring patch description """ api.register_blueprint(blp) spec = api.spec.to_dict() path = spec['paths']['/test/'] assert path['get']['summary'] == 'Docstring get summary' assert 'description' not in path['get'] assert path['put']['summary'] == 'Docstring put summary' assert path['put']['description'] == 'Docstring put description' # @doc decorator overrides docstring assert path['patch']['summary'] == 'Decorator patch summary' assert path['patch']['description'] == 'Decorator patch description'
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_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 = 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 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_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_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 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') def func(document, query_args): return jsonify({ '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 'field' in parameters[2]['schema']['properties'] else: assert len(parameters) == 2 assert 'field' in spec['paths']['/test/']['post']['requestBody'][ 'content']['application/json']['schema']['properties'] # Check parameters are passed as arguments to view function item_data = {'field': 12} response = client.post('/test/', data=json.dumps(item_data), content_type='application/json', query_string={'arg1': 'test'}) assert response.status_code == 200 assert response.json == { 'document': { 'db_field': 12 }, 'query_args': { 'arg1': 'test' }, }
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_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_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' } }, ]
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 = client.get('/test/response') assert response.status_code == 201 assert response.status == '201 CREATED' assert response.json == {} assert response.headers['X-header'] == 'test'
def test_blueprint_response_schema(self, app, openapi_version, schemas): """Check response schema is correctly documented. More specifically, check that: - plural response is documented as array in the spec - schema is document in the right place w.r.t. OpenAPI version """ app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') api.definition('Doc')(schemas.DocSchema) @blp.route('/schema_many_false') @blp.response(schemas.DocSchema(many=False)) def many_false(): pass @blp.route('/schema_many_true') @blp.response(schemas.DocSchema(many=True)) def many_true(): pass api.register_blueprint(blp) paths = api.spec.to_dict()['paths'] response = paths['/test/schema_many_false']['get']['responses'][200] if openapi_version == '2.0': schema = response['schema'] assert schema == {'$ref': '#/definitions/Doc'} else: schema = (response['content']['application/json']['schema']) assert schema == {'$ref': '#/components/schemas/Doc'} response = paths['/test/schema_many_true']['get']['responses'][200] if openapi_version == '2.0': schema = response['schema']['items'] assert schema == {'$ref': '#/definitions/Doc'} else: schema = ( response['content']['application/json']['schema']['items']) assert schema == {'$ref': '#/components/schemas/Doc'}
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() 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 = client.get('/test/') assert response.json == {} assert response.get_etag() == (blp._generate_etag('test'), False)
def test_blueprint_multiple_registrations(self, app, openapi_version): """Check blueprint can be registered multiple times The internal doc structure is modified during the reigistration process. If it is not deepcopied, the second registration fails. """ app.config['OPENAPI_VERSION'] = openapi_version blp = Blueprint('test', __name__, url_prefix='/test') @blp.route('/') def func(): pass api = Api(app) api.register_blueprint(blp) spec_1 = api.spec.to_dict() api = Api(app) api.register_blueprint(blp) spec_2 = api.spec.to_dict() assert spec_1 == spec_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'