├── requirements.txt ├── setup.cfg ├── requirements-dev.txt ├── tox.ini ├── example ├── README.md ├── app.py └── swagger.yml ├── MANIFEST.in ├── .gitignore ├── LICENSE ├── setup.py ├── README.rst ├── bottle_swagger.py └── test └── test_bottle_swagger.py /requirements.txt: -------------------------------------------------------------------------------- 1 | bottle 2 | bravado-core 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | tox 3 | webtest 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py27,py34,py35,cov 3 | 4 | [testenv] 5 | commands=python -m unittest discover test 6 | deps=-rrequirements-dev.txt 7 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Example Bottle-Swagger App 2 | -------------------------- 3 | 4 | To run: 5 | 6 | ```bash 7 | $ python -m bottle app 8 | ``` 9 | 10 | Now open http://localhost:8080/thing 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include README.rst 4 | include requirements.txt 5 | include bottle_swagger.py 6 | 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | recursive-exclude * *.orig 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | # Pycharm 65 | .idea/ 66 | 67 | # Misc 68 | .direnv/ 69 | .envrc 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Charles Blaxland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/app.py: -------------------------------------------------------------------------------- 1 | import bottle 2 | import os 3 | import yaml 4 | from bottle_swagger import SwaggerPlugin 5 | 6 | this_dir = os.path.dirname(os.path.abspath(__file__)) 7 | with open("{}/swagger.yml".format(this_dir)) as f: 8 | swagger_def = yaml.load(f) 9 | 10 | bottle.install(SwaggerPlugin(swagger_def)) 11 | 12 | 13 | @bottle.get('/thing') 14 | def hello(): 15 | return {"id": "1", "name": "Thing1"} 16 | 17 | 18 | @bottle.get('/thing/') 19 | def hello(thing_id): 20 | return {"id": thing_id, "name": "Thing{}".format(thing_id)} 21 | 22 | 23 | @bottle.get('/thing_query') 24 | def hello(): 25 | thing_id = bottle.request.query['thing_id'] 26 | return {"id": thing_id, "name": "Thing{}".format(thing_id)} 27 | 28 | 29 | @bottle.get('/thing_header') 30 | def hello(): 31 | thing_id = bottle.request.headers['thing_id'] 32 | return {"id": thing_id, "name": "Thing{}".format(thing_id)} 33 | 34 | 35 | @bottle.post('/thing_formdata') 36 | def hello(): 37 | thing_id = bottle.request.forms['thing_id'] 38 | return {"id": thing_id, "name": "Thing{}".format(thing_id)} 39 | 40 | 41 | @bottle.post('/thing') 42 | def hello(): 43 | return bottle.request.json 44 | 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from setuptools import setup 4 | 5 | def _read(fname): 6 | try: 7 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 8 | except IOError: 9 | return '' 10 | 11 | REQUIREMENTS = [l for l in _read('requirements.txt').split('\n') if l and not l.startswith('#')] 12 | VERSION = '1.2.0' 13 | 14 | setup( 15 | name='bottle-swagger', 16 | version=VERSION, 17 | url='https://github.com/ampedandwired/bottle-swagger', 18 | download_url='https://github.com/ampedandwired/bottle-swagger/archive/v{}.tar.gz'.format(VERSION), 19 | description='Swagger integration for Bottle', 20 | author='Charles Blaxland', 21 | author_email='charles.blaxland@gmail.com', 22 | license='MIT', 23 | platforms='any', 24 | py_modules=['bottle_swagger'], 25 | install_requires=REQUIREMENTS, 26 | classifiers=[ 27 | 'Environment :: Web Environment', 28 | 'Environment :: Plugins', 29 | 'Framework :: Bottle', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python', 34 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 35 | 'Topic :: Software Development :: Libraries :: Python Modules' 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /example/swagger.yml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: {title: bottle-swagger, version: 1.0.0} 3 | produces: [application/json] 4 | consumes: [application/json] 5 | definitions: 6 | Thing: 7 | properties: 8 | id: {type: string} 9 | name: {type: string} 10 | required: [id] 11 | type: object 12 | paths: 13 | /thing: 14 | get: 15 | responses: 16 | '200': 17 | description: '' 18 | schema: {$ref: '#/definitions/Thing'} 19 | post: 20 | parameters: 21 | - in: body 22 | name: thing 23 | required: true 24 | schema: {$ref: '#/definitions/Thing'} 25 | responses: 26 | '200': 27 | description: '' 28 | schema: {$ref: '#/definitions/Thing'} 29 | "/thing/{thing_id}": 30 | get: 31 | parameters: 32 | - {in: path, name: thing_id, required: true, type: string} 33 | responses: 34 | '200': 35 | description: '' 36 | schema: {$ref: '#/definitions/Thing'} 37 | "/thing_query": 38 | get: 39 | parameters: 40 | - {in: query, name: thing_id, required: true, type: string} 41 | responses: 42 | '200': 43 | description: '' 44 | schema: {$ref: '#/definitions/Thing'} 45 | "/thing_header": 46 | get: 47 | parameters: 48 | - {in: header, name: thing_id, required: true, type: string} 49 | responses: 50 | '200': 51 | description: '' 52 | schema: {$ref: '#/definitions/Thing'} 53 | "/thing_formdata": 54 | post: 55 | consumes: ['application/x-www-form-urlencoded', 'multipart/form-data'] 56 | parameters: 57 | - {in: formData, name: thing_id, required: true, type: string} 58 | responses: 59 | '200': 60 | description: '' 61 | schema: {$ref: '#/definitions/Thing'} 62 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Bottle Swagger Plugin 3 | ===================== 4 | 5 | About 6 | ----- 7 | This project is a Bottle plugin for working with Swagger. 8 | `Bottle `_ is a Python web framework. 9 | `Swagger (OpenAPI) `_ is a standard for defining REST APIs. 10 | 11 | So if you are serving a REST API with Bottle, 12 | and you have a defined a Swagger schema for that API, 13 | this plugin can: 14 | 15 | * Validate incoming requests and outgoing responses against the swagger schema 16 | * Return appropriate error responses on validation failures 17 | * Serve your swagger schema via Bottle (for use in `Swagger UI `_ for example) 18 | 19 | Requirements 20 | ------------ 21 | 22 | * Python >= 2.7 23 | * Bottle >= 0.12 24 | * Swagger specification >= 2.0 25 | 26 | This project relies on `bravado-core `_ to perform the swagger schema validation, 27 | so any version of the Swagger spec supported by that project is also supported by this plugin. 28 | 29 | Installation 30 | ------------ 31 | :: 32 | 33 | $ pip install bottle-swagger 34 | 35 | Usage 36 | ----- 37 | See the "example" directory for a working example of using this plugin. 38 | 39 | The simplest usage is:: 40 | 41 | import bottle 42 | 43 | swagger_def = _load_swagger_def() 44 | bottle.install(SwaggerPlugin(swagger_def)) 45 | 46 | Where "_load_swagger_def" returns a dict representing your swagger specification 47 | (loaded from a yaml file, for example). 48 | 49 | There are a number of arguments that you can pass to the plugin constructor: 50 | 51 | * ``validate_requests`` - Boolean (default ``True``) indicating if incoming requests should be validated or not 52 | * ``validate_responses`` - Boolean (default ``True``) indicating if outgoing responses should be validated or not 53 | * ``ignore_undefined_routes`` - Boolean (default ``False``) indicating if undefined routes 54 | (that is, routes not defined in the swagger spec) should be passed on ("True") or return a 404 ("False") 55 | * ``invalid_request_handler`` - Callback called when request validation has failed. 56 | Default behaviour is to return a "400 Bad Request" response. 57 | * ``invalid_response_handler`` - Callback called when response validation has failed. 58 | Default behaviour is to return a "500 Server Error" response. 59 | * ``swagger_op_not_found_handler`` - Callback called when no swagger operation matching the request was found in the swagger schema. 60 | Default behaviour is to return a "404 Not Found" response. 61 | * ``exception_handler=_server_error_handler`` - Callback called when an exception is thrown by downstream handlers (including exceptions thrown by your code). 62 | Default behaviour is to return a "500 Server Error" response. 63 | * ``serve_swagger_schema`` - Boolean (default ``True``) indicating if the Swagger schema JSON should be served 64 | * ``swagger_schema_url`` - URL (default ``/swagger.json``) on which to serve the Swagger schema JSON 65 | 66 | All the callbacks above receive a single parameter representing the ``Exception`` that was raised, 67 | or in the case of ``swagger_op_not_found_handler`` the ``Route`` that was not found. 68 | They should all return a Bottle ``Response`` object. 69 | 70 | Contributing 71 | ------------ 72 | Development happens in the `bottle-swagger GitHub respository `_. 73 | Pull requests (with accompanying unit tests), feature suggestions and bug reports are welcome. 74 | 75 | Use "tox" to run the unit tests:: 76 | 77 | $ tox 78 | -------------------------------------------------------------------------------- /bottle_swagger.py: -------------------------------------------------------------------------------- 1 | import re 2 | from bottle import request, response, HTTPResponse 3 | from bravado_core.exception import MatchingResponseNotFound 4 | from bravado_core.request import IncomingRequest, unmarshal_request 5 | from bravado_core.response import OutgoingResponse, validate_response, get_response_spec 6 | from bravado_core.spec import Spec 7 | from jsonschema import ValidationError 8 | 9 | 10 | def _error_response(status, e): 11 | response.status = status 12 | return {"code": status, "message": str(e)} 13 | 14 | 15 | def _server_error_handler(e): 16 | return _error_response(500, e) 17 | 18 | 19 | def _bad_request_handler(e): 20 | return _error_response(400, e) 21 | 22 | 23 | def _not_found_handler(e): 24 | return _error_response(404, e) 25 | 26 | 27 | class SwaggerPlugin: 28 | DEFAULT_SWAGGER_SCHEMA_URL = '/swagger.json' 29 | 30 | name = 'swagger' 31 | api = 2 32 | 33 | def __init__(self, swagger_def, 34 | validate_requests=True, 35 | validate_responses=True, 36 | ignore_undefined_routes=False, 37 | invalid_request_handler=_bad_request_handler, 38 | invalid_response_handler=_server_error_handler, 39 | swagger_op_not_found_handler=_not_found_handler, 40 | exception_handler=_server_error_handler, 41 | serve_swagger_schema=True, 42 | swagger_schema_url=DEFAULT_SWAGGER_SCHEMA_URL): 43 | self.swagger = Spec.from_dict(swagger_def) 44 | self.validate_requests = validate_requests 45 | self.validate_responses = validate_responses 46 | self.ignore_undefined_routes = ignore_undefined_routes 47 | self.invalid_request_handler = invalid_request_handler 48 | self.invalid_response_handler = invalid_response_handler 49 | self.swagger_op_not_found_handler = swagger_op_not_found_handler 50 | self.exception_handler = exception_handler 51 | self.serve_swagger_schema = serve_swagger_schema 52 | self.swagger_schema_url = swagger_schema_url 53 | 54 | def apply(self, callback, route): 55 | def wrapper(*args, **kwargs): 56 | return self._swagger_validate(callback, route, *args, **kwargs) 57 | 58 | return wrapper 59 | 60 | def setup(self, app): 61 | if self.serve_swagger_schema: 62 | @app.get(self.swagger_schema_url) 63 | def swagger_schema(): 64 | return self.swagger.spec_dict 65 | 66 | def _swagger_validate(self, callback, route, *args, **kwargs): 67 | swagger_op = self._swagger_op(route) 68 | if not swagger_op: 69 | if self.ignore_undefined_routes or self._is_swagger_schema_route(route): 70 | return callback(*args, **kwargs) 71 | else: 72 | return self.swagger_op_not_found_handler(route) 73 | 74 | try: 75 | if self.validate_requests: 76 | try: 77 | self._validate_request(swagger_op) 78 | except ValidationError as e: 79 | return self.invalid_request_handler(e) 80 | 81 | result = callback(*args, **kwargs) 82 | 83 | if self.validate_responses: 84 | try: 85 | self._validate_response(swagger_op, result) 86 | except (ValidationError, MatchingResponseNotFound) as e: 87 | return self.invalid_response_handler(e) 88 | 89 | except Exception as e: 90 | # Bottle handles redirects by raising an HTTPResponse instance 91 | if isinstance(e, HTTPResponse): 92 | raise e 93 | 94 | return self.exception_handler(e) 95 | 96 | return result 97 | 98 | @staticmethod 99 | def _validate_request(swagger_op): 100 | unmarshal_request(BottleIncomingRequest(request), swagger_op) 101 | 102 | @staticmethod 103 | def _validate_response(swagger_op, result): 104 | response_spec = get_response_spec(int(response.status_code), swagger_op) 105 | outgoing_response = BottleOutgoingResponse(response, result) 106 | validate_response(response_spec, swagger_op, outgoing_response) 107 | 108 | def _swagger_op(self, route): 109 | # Convert bottle "" style path params to swagger "{param}" style 110 | path = re.sub(r'/<(.+?)>', r'/{\1}', route.rule) 111 | return self.swagger.get_op_for_request(request.method, path) 112 | 113 | def _is_swagger_schema_route(self, route): 114 | return self.serve_swagger_schema and route.rule == self.swagger_schema_url 115 | 116 | 117 | class BottleIncomingRequest(IncomingRequest): 118 | def __init__(self, bottle_request): 119 | self.request = bottle_request 120 | self.path = bottle_request.url_args 121 | 122 | def json(self): 123 | return self.request.json 124 | 125 | @property 126 | def query(self): 127 | return self.request.query 128 | 129 | @property 130 | def headers(self): 131 | return self.request.headers 132 | 133 | @property 134 | def form(self): 135 | return self.request.forms 136 | 137 | class BottleOutgoingResponse(OutgoingResponse): 138 | def __init__(self, bottle_response, response_json): 139 | self.response = bottle_response 140 | self.response_json = response_json 141 | self.content_type = bottle_response.content_type if bottle_response.content_type else 'application/json' 142 | 143 | def json(self): 144 | return self.response_json 145 | -------------------------------------------------------------------------------- /test/test_bottle_swagger.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from bottle import Bottle, redirect 4 | from bottle_swagger import SwaggerPlugin 5 | from webtest import TestApp 6 | 7 | 8 | class TestBottleSwagger(TestCase): 9 | VALID_JSON = {"id": "123", "name": "foo"} 10 | INVALID_JSON = {"not_id": "123", "name": "foo"} 11 | 12 | SWAGGER_DEF = { 13 | "swagger": "2.0", 14 | "info": {"version": "1.0.0", "title": "bottle-swagger"}, 15 | "consumes": ["application/json"], 16 | "produces": ["application/json"], 17 | "definitions": { 18 | "Thing": { 19 | "type": "object", 20 | "required": ["id"], 21 | "properties": { 22 | "id": {"type": "string"}, 23 | "name": {"type": "string"} 24 | } 25 | } 26 | }, 27 | "paths": { 28 | "/thing": { 29 | "get": { 30 | "responses": { 31 | "200": { 32 | "description": "", 33 | "schema": { 34 | "$ref": "#/definitions/Thing" 35 | } 36 | } 37 | } 38 | }, 39 | "post": { 40 | "parameters": [{ 41 | "name": "thing", 42 | "in": "body", 43 | "required": True, 44 | "schema": { 45 | "$ref": "#/definitions/Thing" 46 | } 47 | }], 48 | "responses": { 49 | "200": { 50 | "description": "", 51 | "schema": { 52 | "$ref": "#/definitions/Thing" 53 | } 54 | } 55 | } 56 | } 57 | }, 58 | "/thing/{thing_id}": { 59 | "get": { 60 | "parameters": [{ 61 | "name": "thing_id", 62 | "in": "path", 63 | "required": True, 64 | "type": "string" 65 | }], 66 | "responses": { 67 | "200": { 68 | "description": "", 69 | "schema": { 70 | "$ref": "#/definitions/Thing" 71 | } 72 | } 73 | } 74 | } 75 | }, 76 | "/thing_query": { 77 | "get": { 78 | "parameters": [{ 79 | "name": "thing_id", 80 | "in": "query", 81 | "required": True, 82 | "type": "string" 83 | }], 84 | "responses": { 85 | "200": { 86 | "description": "", 87 | "schema": { 88 | "$ref": "#/definitions/Thing" 89 | } 90 | } 91 | } 92 | } 93 | }, 94 | "/thing_header": { 95 | "get": { 96 | "parameters": [{ 97 | "name": "thing_id", 98 | "in": "header", 99 | "required": True, 100 | "type": "string" 101 | }], 102 | "responses": { 103 | "200": { 104 | "description": "", 105 | "schema": { 106 | "$ref": "#/definitions/Thing" 107 | } 108 | } 109 | } 110 | } 111 | }, 112 | "/thing_formdata": { 113 | "post": { 114 | "consumes": [ 115 | "application/x-www-form-urlencoded", 116 | "multipart/form-data" 117 | ], 118 | "parameters": [{ 119 | "name": "thing_id", 120 | "in": "formData", 121 | "required": True, 122 | "type": "string" 123 | }], 124 | "responses": { 125 | "200": { 126 | "description": "", 127 | "schema": { 128 | "$ref": "#/definitions/Thing" 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | def test_valid_get_request_and_response(self): 138 | response = self._test_request() 139 | self.assertEqual(response.status_int, 200) 140 | 141 | def test_valid_post_request_and_response(self): 142 | response = self._test_request(method='POST') 143 | self.assertEqual(response.status_int, 200) 144 | 145 | def test_invalid_request(self): 146 | response = self._test_request(method='POST', request_json=self.INVALID_JSON) 147 | self._assert_error_response(response, 400) 148 | 149 | def test_invalid_response(self): 150 | response = self._test_request(response_json=self.INVALID_JSON) 151 | self._assert_error_response(response, 500) 152 | 153 | def test_disable_request_validation(self): 154 | self._test_disable_validation(validate_requests=False, expected_request_status=200, 155 | expected_response_status=500) 156 | 157 | def test_disable_response_validation(self): 158 | self._test_disable_validation(validate_responses=False, expected_request_status=400, 159 | expected_response_status=200) 160 | 161 | def test_disable_all_validation(self): 162 | self._test_disable_validation(validate_requests=False, validate_responses=False, expected_request_status=200, 163 | expected_response_status=200) 164 | 165 | def test_exception_handling(self): 166 | def throw_ex(): 167 | raise Exception("Exception occurred") 168 | 169 | response = self._test_request(response_json=throw_ex) 170 | self._assert_error_response(response, 500) 171 | 172 | def test_invalid_route(self): 173 | response = self._test_request(url="/invalid") 174 | self._assert_error_response(response, 404) 175 | 176 | def test_ignore_invalid_route(self): 177 | swagger_plugin = self._make_swagger_plugin(ignore_undefined_routes=True) 178 | response = self._test_request(swagger_plugin=swagger_plugin, url="/invalid") 179 | self.assertEqual(response.status_int, 200) 180 | response = self._test_request(swagger_plugin=swagger_plugin, url="/invalid", method='POST', 181 | request_json=self.INVALID_JSON, response_json=self.INVALID_JSON) 182 | self.assertEqual(response.status_int, 200) 183 | 184 | def test_redirects(self): 185 | def _test_redirect(swagger_plugin): 186 | def redir(): 187 | redirect("/actual_thing") 188 | 189 | response = self._test_request(response_json=redir, swagger_plugin=swagger_plugin) 190 | self.assertEqual(response.status_int, 302) 191 | 192 | _test_redirect(self._make_swagger_plugin()) 193 | _test_redirect(self._make_swagger_plugin(ignore_undefined_routes=True)) 194 | 195 | def test_path_parameters(self): 196 | response = self._test_request(url="/thing/123", route_url="/thing/") 197 | self.assertEqual(response.status_int, 200) 198 | 199 | def test_query_parameters(self): 200 | response = self._test_request(url="/thing_query?thing_id=123", route_url="/thing_query") 201 | self.assertEqual(response.status_int, 200) 202 | 203 | def test_header_parameters(self): 204 | response = self._test_request(url="/thing_header", route_url="/thing_header", headers={'thing_id': '123'}) 205 | self.assertEqual(response.status_int, 200) 206 | 207 | def test_formdata_parameters(self): 208 | response = self._test_request(url="/thing_formdata", route_url="/thing_formdata", method='POST', request_json='thing_id=123', content_type='multipart/form-data') 209 | self.assertEqual(response.status_int, 200) 210 | 211 | def test_get_swagger_schema(self): 212 | bottle_app = Bottle() 213 | bottle_app.install(self._make_swagger_plugin()) 214 | test_app = TestApp(bottle_app) 215 | response = test_app.get(SwaggerPlugin.DEFAULT_SWAGGER_SCHEMA_URL) 216 | self.assertEquals(response.json, self.SWAGGER_DEF) 217 | 218 | def _test_request(self, swagger_plugin=None, method='GET', url='/thing', route_url=None, request_json=VALID_JSON, 219 | response_json=VALID_JSON, headers=None, content_type='application/json'): 220 | if swagger_plugin is None: 221 | swagger_plugin = self._make_swagger_plugin() 222 | if response_json is None: 223 | response_json = {} 224 | if route_url is None: 225 | route_url = url 226 | 227 | bottle_app = Bottle() 228 | bottle_app.install(swagger_plugin) 229 | 230 | @bottle_app.route(route_url, method) 231 | def do_thing(*args, **kwargs): 232 | return response_json() if hasattr(response_json, "__call__") else response_json 233 | 234 | test_app = TestApp(bottle_app) 235 | if method.upper() == 'GET': 236 | response = test_app.get(url, expect_errors=True, headers=headers) 237 | elif method.upper() == 'POST': 238 | if content_type == 'application/json': 239 | response = test_app.post_json(url, request_json, expect_errors=True, headers=headers) 240 | else: 241 | response = test_app.post(url, request_json, content_type=content_type, expect_errors=True, headers=headers) 242 | else: 243 | 244 | raise Exception("Invalid method {}".format(method)) 245 | 246 | return response 247 | 248 | def _test_disable_validation(self, validate_requests=True, validate_responses=True, expected_request_status=200, 249 | expected_response_status=200): 250 | swagger_plugin = self._make_swagger_plugin(validate_requests=validate_requests, 251 | validate_responses=validate_responses) 252 | 253 | response = self._test_request(swagger_plugin=swagger_plugin, method='POST', request_json=self.INVALID_JSON) 254 | self.assertEqual(response.status_int, expected_request_status) 255 | 256 | response = self._test_request(swagger_plugin=swagger_plugin, response_json=self.INVALID_JSON) 257 | self.assertEqual(response.status_int, expected_response_status) 258 | 259 | def _assert_error_response(self, response, expected_status): 260 | self.assertEqual(response.status_int, expected_status) 261 | self.assertEqual(response.json['code'], expected_status) 262 | self.assertIsNotNone(response.json['message']) 263 | 264 | def _make_swagger_plugin(self, *args, **kwargs): 265 | return SwaggerPlugin(self.SWAGGER_DEF, *args, **kwargs) 266 | --------------------------------------------------------------------------------