async def test_set_etag_method_not_allowed_warning(self, app, method, etag_disabled): app.config['ETAG_DISABLED'] = etag_disabled blp = Blueprint('test', __name__) with mock.patch.object(app.logger, 'warning') as mock_warning: async with app.test_request_context('/', method=method): blp.set_etag(None) if method in ['GET', 'HEAD', 'POST', 'PUT', 'PATCH']: assert not mock_warning.called else: assert mock_warning.called
async def test_etag_set_etag_in_response(self, app, schemas, paginate): blp = Blueprint('test', __name__) etag_schema = schemas.DocEtagSchema item = {'item_id': 1, 'db_field': 0} if paginate: extra_data = (('X-Pagination', 'Dummy pagination header'), ) else: extra_data = tuple() etag = blp._generate_etag(item, extra_data=extra_data) etag_with_schema = blp._generate_etag(item, etag_schema, extra_data=extra_data) async with app.test_request_context('/'): resp = Response([]) if extra_data: resp.headers['X-Pagination'] = 'Dummy pagination header' get_appcontext()['result_dump'] = item blp._set_etag_in_response(resp, None) assert resp.get_etag() == (etag, False) async with app.test_request_context('/'): resp = Response([]) if extra_data: resp.headers['X-Pagination'] = 'Dummy pagination header' get_appcontext()['result_raw'] = item blp._set_etag_in_response(resp, etag_schema) assert resp.get_etag() == (etag_with_schema, False)
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_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_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
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_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']
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' }, } })
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_etag_is_deterministic(self): """Check etag computation is deterministic _generate_etag should return the same value everytime the same dictionary is passed. This is not obvious since dictionaries are unordered by design. We check this by feeding it different OrderedDict instances that are equivalent to the same dictionary. """ blp = Blueprint('test', __name__) data = OrderedDict([('a', 1), ('b', 2), ('c', OrderedDict([('a', 1), ('b', 2)]))]) etag = blp._generate_etag(data) data_copies = [ OrderedDict([ ('b', 2), ('a', 1), ('c', OrderedDict([('a', 1), ('b', 2)])), ]), OrderedDict([ ('a', 1), ('b', 2), ('c', OrderedDict([('b', 2), ('a', 1)])), ]), OrderedDict([ ('a', 1), ('c', OrderedDict([('a', 1), ('b', 2)])), ('b', 2), ]), OrderedDict([ ('c', OrderedDict([('a', 1), ('b', 2)])), ('b', 2), ('a', 1), ]), ] data_copies_etag = [blp._generate_etag(d) for d in data_copies] assert all(e == etag for e in data_copies_etag)
async def test_etag_verify_check_etag_warning(self, app, method): blp = Blueprint('test', __name__) old_item = {'item_id': 1, 'db_field': 0} old_etag = blp._generate_etag(old_item) with mock.patch.object(app.logger, 'warning') as mock_warning: async with app.test_request_context('/', method=method, headers={'If-Match': old_etag}): blp._verify_check_etag() if method in ['PUT', 'PATCH', 'DELETE']: assert mock_warning.called mock_warning.reset_mock() else: assert not mock_warning.called blp.check_etag(old_item) blp._verify_check_etag() assert not mock_warning.called
def test_etag_generate_etag(self, schemas, extra_data): blp = Blueprint('test', __name__) etag_schema = schemas.DocEtagSchema item = {'item_id': 1, 'db_field': 0} item_schema_dump = etag_schema().dump(item) if MARSHMALLOW_VERSION_MAJOR < 3: item_schema_dump = item_schema_dump[0] if extra_data is None or extra_data == {}: data = item data_dump = item_schema_dump else: data = (item, extra_data) data_dump = (item_schema_dump, extra_data) etag = blp._generate_etag(item, extra_data=extra_data) assert etag == hashlib.sha1( bytes(json.dumps(data, sort_keys=True), 'utf-8')).hexdigest() etag = blp._generate_etag(item, etag_schema, extra_data=extra_data) assert etag == hashlib.sha1( bytes(json.dumps(data_dump, sort_keys=True), 'utf-8')).hexdigest() etag = blp._generate_etag(item, etag_schema(), extra_data=extra_data) assert etag == hashlib.sha1( bytes(json.dumps(data_dump, sort_keys=True), 'utf-8')).hexdigest()
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
async def test_etag_check_precondition(self, app, method): blp = Blueprint('test', __name__) async with app.test_request_context('/', method=method): if method in ['PUT', 'PATCH', 'DELETE']: with pytest.raises(PreconditionRequired): blp._check_precondition() else: blp._check_precondition()
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_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_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_verify_check_etag_exception(self, app, method, debug, testing): app.config['DEBUG'] = debug app.config['TESTING'] = testing blp = Blueprint('test', __name__) async with app.test_request_context('/', method=method): if (debug or testing) and method in ['PUT', 'PATCH', 'DELETE']: with pytest.raises(CheckEtagNotCalledError, match='ETag not checked in endpoint'): blp._verify_check_etag() else: blp._verify_check_etag()
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'
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_example(self, app, openapi_version): app.config['OPENAPI_VERSION'] = openapi_version api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test') example = {'name': 'One'} @blp.route('/') @blp.response(example=example) def func(): pass api.register_blueprint(blp) get = api.spec.to_dict()['paths']['/test/']['get'] if openapi_version == '2.0': assert get['responses']['200']['examples'][ 'application/json'] == example else: assert get['responses']['200']['content']['application/json'][ 'example'] == example
async def test_api_register_blueprint_options(self, app): api = Api(app) blp = Blueprint('test', 'test', url_prefix='/test1') @blp.route('/') def test_func(): return {'response': 'OK'} api.register_blueprint(blp, url_prefix='/test2') spec = api.spec.to_dict() assert '/test1/' not in spec['paths'] assert '/test2/' in spec['paths'] client = app.test_client() response = await client.get('/test1/') assert response.status_code == 404 response = await client.get('/test2/') assert response.status_code == 200 json = await response.json assert json == {'response': 'OK'}
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') @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_ref = build_ref(api.spec, 'schema', 'Doc') response = paths['/test/schema_many_false']['get']['responses']['200'] if openapi_version == '2.0': assert response['schema'] == schema_ref else: assert (response['content']['application/json']['schema'] == schema_ref) response = paths['/test/schema_many_true']['get']['responses']['200'] if openapi_version == '2.0': assert response['schema']['items'] == schema_ref else: assert (response['content']['application/json']['schema']['items'] == schema_ref)
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""" location = location or 'json' api.register_blueprint(blp) spec = api.spec.to_dict() get = spec['paths']['/test/']['get'] if (openapi_version == '3.0.2' and location in REQUEST_BODY_CONTENT_TYPE): assert 'parameters' not in get assert 'requestBody' in get assert len(get['requestBody']['content']) == 1 assert REQUEST_BODY_CONTENT_TYPE[location] in get['requestBody'][ 'content'] else: loc = get['parameters'][0]['in'] assert loc == openapi_location assert 'requestBody' not in get if location in REQUEST_BODY_CONTENT_TYPE and location != 'json': assert get['consumes'] == [REQUEST_BODY_CONTENT_TYPE[location]] 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']