mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-10 04:19:37 +00:00
Converting response to raise a ProblemException (#955)
* Converting response to raise a ProblemException * Centralizing around ProblemException for errors in the app. * Adding the ability to skip error handlers, allow for defining exception payload. * Fixing flake8 * Fixed some bugs found through unit testing. * Unit tests are now passing. * Added problem back to __init__ * Updating based on the feedback from the PR.
This commit is contained in:
committed by
Henning Jacobs
parent
c8d8973c7e
commit
b0b83c4879
@@ -55,7 +55,8 @@ def problems_middleware(request, handler):
|
|||||||
try:
|
try:
|
||||||
response = yield from handler(request)
|
response = yield from handler(request)
|
||||||
except ProblemException as exc:
|
except ProblemException as exc:
|
||||||
response = exc.to_problem()
|
response = problem(status=exc.status, detail=exc.detail, title=exc.title,
|
||||||
|
type=exc.type, instance=exc.instance, headers=exc.headers, ext=exc.ext)
|
||||||
except (werkzeug_HTTPException, _HttpNotFoundError) as exc:
|
except (werkzeug_HTTPException, _HttpNotFoundError) as exc:
|
||||||
response = problem(status=exc.code, title=exc.name, detail=exc.description)
|
response = problem(status=exc.code, title=exc.name, detail=exc.description)
|
||||||
except web.HTTPError as exc:
|
except web.HTTPError as exc:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ logger = logging.getLogger('connexion.app')
|
|||||||
class AbstractApp(object):
|
class AbstractApp(object):
|
||||||
def __init__(self, import_name, api_cls, port=None, specification_dir='',
|
def __init__(self, import_name, api_cls, port=None, specification_dir='',
|
||||||
host=None, server=None, arguments=None, auth_all_paths=False, debug=False,
|
host=None, server=None, arguments=None, auth_all_paths=False, debug=False,
|
||||||
resolver=None, options=None):
|
resolver=None, options=None, skip_error_handlers=False):
|
||||||
"""
|
"""
|
||||||
:param import_name: the name of the application package
|
:param import_name: the name of the application package
|
||||||
:type import_name: str
|
:type import_name: str
|
||||||
@@ -63,8 +63,9 @@ class AbstractApp(object):
|
|||||||
|
|
||||||
logger.debug('Specification directory: %s', self.specification_dir)
|
logger.debug('Specification directory: %s', self.specification_dir)
|
||||||
|
|
||||||
logger.debug('Setting error handlers')
|
if not skip_error_handlers:
|
||||||
self.set_errors_handlers()
|
logger.debug('Setting error handlers')
|
||||||
|
self.set_errors_handlers()
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def create_app(self):
|
def create_app(self):
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ class FlaskApp(AbstractApp):
|
|||||||
:type exception: Exception
|
:type exception: Exception
|
||||||
"""
|
"""
|
||||||
if isinstance(exception, ProblemException):
|
if isinstance(exception, ProblemException):
|
||||||
response = exception.to_problem()
|
response = problem(
|
||||||
|
status=exception.status, title=exception.title, detail=exception.detail,
|
||||||
|
type=exception.type, instance=exception.instance, headers=exception.headers,
|
||||||
|
ext=exception.ext)
|
||||||
else:
|
else:
|
||||||
if not isinstance(exception, werkzeug.exceptions.HTTPException):
|
if not isinstance(exception, werkzeug.exceptions.HTTPException):
|
||||||
exception = werkzeug.exceptions.InternalServerError()
|
exception = werkzeug.exceptions.InternalServerError()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
from connexion.exceptions import ProblemException
|
||||||
try:
|
try:
|
||||||
import uwsgi_metrics
|
import uwsgi_metrics
|
||||||
HAS_UWSGI_METRICS = True # pragma: no cover
|
HAS_UWSGI_METRICS = True # pragma: no cover
|
||||||
@@ -40,6 +40,9 @@ class UWSGIMetricsCollector(object):
|
|||||||
except HTTPException as http_e:
|
except HTTPException as http_e:
|
||||||
status = http_e.code
|
status = http_e.code
|
||||||
raise http_e
|
raise http_e
|
||||||
|
except ProblemException as prob_e:
|
||||||
|
status = prob_e.status
|
||||||
|
raise prob_e
|
||||||
finally:
|
finally:
|
||||||
end_time_s = time.time()
|
end_time_s = time.time()
|
||||||
delta_s = end_time_s - start_time_s
|
delta_s = end_time_s - start_time_s
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from jsonschema import ValidationError
|
|||||||
|
|
||||||
from ..exceptions import (NonConformingResponseBody,
|
from ..exceptions import (NonConformingResponseBody,
|
||||||
NonConformingResponseHeaders)
|
NonConformingResponseHeaders)
|
||||||
from ..problem import problem
|
|
||||||
from ..utils import all_json, has_coroutine
|
from ..utils import all_json, has_coroutine
|
||||||
from .decorator import BaseDecorator
|
from .decorator import BaseDecorator
|
||||||
from .validation import ResponseBodyValidator
|
from .validation import ResponseBodyValidator
|
||||||
@@ -86,16 +85,11 @@ class ResponseValidator(BaseDecorator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def _wrapper(request, response):
|
def _wrapper(request, response):
|
||||||
try:
|
connexion_response = \
|
||||||
connexion_response = \
|
self.operation.api.get_connexion_response(response, self.mimetype)
|
||||||
self.operation.api.get_connexion_response(response, self.mimetype)
|
self.validate_response(
|
||||||
self.validate_response(
|
connexion_response.body, connexion_response.status_code,
|
||||||
connexion_response.body, connexion_response.status_code,
|
connexion_response.headers, request.url)
|
||||||
connexion_response.headers, request.url)
|
|
||||||
|
|
||||||
except (NonConformingResponseBody, NonConformingResponseHeaders) as e:
|
|
||||||
response = problem(500, e.reason, e.message)
|
|
||||||
return self.operation.api.get_response(response)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ from jsonschema import Draft4Validator, ValidationError, draft4_format_checker
|
|||||||
from jsonschema.validators import extend
|
from jsonschema.validators import extend
|
||||||
from werkzeug.datastructures import FileStorage
|
from werkzeug.datastructures import FileStorage
|
||||||
|
|
||||||
from ..exceptions import ExtraParameterProblem
|
from ..exceptions import ExtraParameterProblem, BadRequestProblem, UnsupportedMediaTypeProblem
|
||||||
from ..http_facts import FORM_CONTENT_TYPES
|
from ..http_facts import FORM_CONTENT_TYPES
|
||||||
from ..json_schema import Draft4RequestValidator, Draft4ResponseValidator
|
from ..json_schema import Draft4RequestValidator, Draft4ResponseValidator
|
||||||
from ..problem import problem
|
|
||||||
from ..utils import all_json, boolean, is_json_mimetype, is_null, is_nullable
|
from ..utils import all_json, boolean, is_json_mimetype, is_null, is_nullable
|
||||||
|
|
||||||
_jsonschema_3_or_newer = pkg_resources.parse_version(
|
_jsonschema_3_or_newer = pkg_resources.parse_version(
|
||||||
@@ -148,22 +147,17 @@ class RequestBodyValidator(object):
|
|||||||
|
|
||||||
if ctype_is_json:
|
if ctype_is_json:
|
||||||
# Content-Type is json but actual body was not parsed
|
# Content-Type is json but actual body was not parsed
|
||||||
return problem(400,
|
raise BadRequestProblem(detail="Request body is not valid JSON")
|
||||||
"Bad Request",
|
|
||||||
"Request body is not valid JSON"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# the body has contents that were not parsed as JSON
|
# the body has contents that were not parsed as JSON
|
||||||
return problem(415,
|
raise UnsupportedMediaTypeProblem(
|
||||||
"Unsupported Media Type",
|
|
||||||
"Invalid Content-type ({content_type}), expected JSON data".format(
|
"Invalid Content-type ({content_type}), expected JSON data".format(
|
||||||
content_type=request.headers.get("Content-Type", "")
|
content_type=request.headers.get("Content-Type", "")
|
||||||
))
|
))
|
||||||
|
|
||||||
logger.debug("%s validating schema...", request.url)
|
logger.debug("%s validating schema...", request.url)
|
||||||
error = self.validate_schema(data, request.url)
|
if data is not None or not self.has_default:
|
||||||
if error and not self.has_default:
|
self.validate_schema(data, request.url)
|
||||||
return error
|
|
||||||
elif self.consumes[0] in FORM_CONTENT_TYPES:
|
elif self.consumes[0] in FORM_CONTENT_TYPES:
|
||||||
data = dict(request.form.items()) or (request.body if len(request.body) > 0 else {})
|
data = dict(request.form.items()) or (request.body if len(request.body) > 0 else {})
|
||||||
data.update(dict.fromkeys(request.files, '')) # validator expects string..
|
data.update(dict.fromkeys(request.files, '')) # validator expects string..
|
||||||
@@ -185,11 +179,9 @@ class RequestBodyValidator(object):
|
|||||||
errs += [str(e)]
|
errs += [str(e)]
|
||||||
print(errs)
|
print(errs)
|
||||||
if errs:
|
if errs:
|
||||||
return problem(400, 'Bad Request', errs)
|
raise BadRequestProblem(detail=errs)
|
||||||
|
|
||||||
error = self.validate_schema(data, request.url)
|
self.validate_schema(data, request.url)
|
||||||
if error:
|
|
||||||
return error
|
|
||||||
|
|
||||||
response = function(request)
|
response = function(request)
|
||||||
return response
|
return response
|
||||||
@@ -207,7 +199,7 @@ class RequestBodyValidator(object):
|
|||||||
logger.error("{url} validation error: {error}".format(url=url,
|
logger.error("{url} validation error: {error}".format(url=url,
|
||||||
error=exception.message),
|
error=exception.message),
|
||||||
extra={'validator': 'body'})
|
extra={'validator': 'body'})
|
||||||
return problem(400, 'Bad Request', str(exception.message))
|
raise BadRequestProblem(detail=str(exception.message))
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -358,32 +350,27 @@ class ParameterValidator(object):
|
|||||||
for param in self.parameters.get('query', []):
|
for param in self.parameters.get('query', []):
|
||||||
error = self.validate_query_parameter(param, request)
|
error = self.validate_query_parameter(param, request)
|
||||||
if error:
|
if error:
|
||||||
response = problem(400, 'Bad Request', error)
|
raise BadRequestProblem(detail=error)
|
||||||
return self.api.get_response(response)
|
|
||||||
|
|
||||||
for param in self.parameters.get('path', []):
|
for param in self.parameters.get('path', []):
|
||||||
error = self.validate_path_parameter(param, request)
|
error = self.validate_path_parameter(param, request)
|
||||||
if error:
|
if error:
|
||||||
response = problem(400, 'Bad Request', error)
|
raise BadRequestProblem(detail=error)
|
||||||
return self.api.get_response(response)
|
|
||||||
|
|
||||||
for param in self.parameters.get('header', []):
|
for param in self.parameters.get('header', []):
|
||||||
error = self.validate_header_parameter(param, request)
|
error = self.validate_header_parameter(param, request)
|
||||||
if error:
|
if error:
|
||||||
response = problem(400, 'Bad Request', error)
|
raise BadRequestProblem(detail=error)
|
||||||
return self.api.get_response(response)
|
|
||||||
|
|
||||||
for param in self.parameters.get('cookie', []):
|
for param in self.parameters.get('cookie', []):
|
||||||
error = self.validate_cookie_parameter(param, request)
|
error = self.validate_cookie_parameter(param, request)
|
||||||
if error:
|
if error:
|
||||||
response = problem(400, 'Bad Request', error)
|
raise BadRequestProblem(detail=error)
|
||||||
return self.api.get_response(response)
|
|
||||||
|
|
||||||
for param in self.parameters.get('formData', []):
|
for param in self.parameters.get('formData', []):
|
||||||
error = self.validate_formdata_parameter(param["name"], param, request)
|
error = self.validate_formdata_parameter(param["name"], param, request)
|
||||||
if error:
|
if error:
|
||||||
response = problem(400, 'Bad Request', error)
|
raise BadRequestProblem(detail=error)
|
||||||
return self.api.get_response(response)
|
|
||||||
|
|
||||||
return function(request)
|
return function(request)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import warnings
|
||||||
from jsonschema.exceptions import ValidationError
|
from jsonschema.exceptions import ValidationError
|
||||||
from werkzeug.exceptions import Forbidden, Unauthorized
|
from werkzeug.exceptions import Forbidden, Unauthorized
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@ class ProblemException(ConnexionException):
|
|||||||
self.ext = ext
|
self.ext = ext
|
||||||
|
|
||||||
def to_problem(self):
|
def to_problem(self):
|
||||||
|
warnings.warn(
|
||||||
|
"'to_problem' is planned to be removed in a future release. "
|
||||||
|
"Call connexion.problem.problem(..) instead to maintain the existing error response.", DeprecationWarning)
|
||||||
return problem(status=self.status, title=self.title, detail=self.detail,
|
return problem(status=self.status, title=self.title, detail=self.detail,
|
||||||
type=self.type, instance=self.instance, headers=self.headers,
|
type=self.type, instance=self.instance, headers=self.headers,
|
||||||
ext=self.ext)
|
ext=self.ext)
|
||||||
@@ -52,12 +56,13 @@ class InvalidSpecification(ConnexionException, ValidationError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NonConformingResponse(ConnexionException):
|
class NonConformingResponse(ProblemException):
|
||||||
def __init__(self, reason='Unknown Reason', message=None):
|
def __init__(self, reason='Unknown Reason', message=None):
|
||||||
"""
|
"""
|
||||||
:param reason: Reason why the response did not conform to the specification
|
:param reason: Reason why the response did not conform to the specification
|
||||||
:type reason: str
|
:type reason: str
|
||||||
"""
|
"""
|
||||||
|
super(NonConformingResponse, self).__init__(status=500, title=reason, detail=message)
|
||||||
self.reason = reason
|
self.reason = reason
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
@@ -68,6 +73,30 @@ class NonConformingResponse(ConnexionException):
|
|||||||
return '<NonConformingResponse: {}>'.format(self.reason)
|
return '<NonConformingResponse: {}>'.format(self.reason)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationProblem(ProblemException):
|
||||||
|
|
||||||
|
def __init__(self, status, title, detail):
|
||||||
|
super(AuthenticationProblem, self).__init__(status=status, title=title, detail=detail)
|
||||||
|
|
||||||
|
|
||||||
|
class ResolverProblem(ProblemException):
|
||||||
|
|
||||||
|
def __init__(self, status, title, detail):
|
||||||
|
super(ResolverProblem, self).__init__(status=status, title=title, detail=detail)
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequestProblem(ProblemException):
|
||||||
|
|
||||||
|
def __init__(self, title='Bad Request', detail=None):
|
||||||
|
super(BadRequestProblem, self).__init__(status=400, title=title, detail=detail)
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedMediaTypeProblem(ProblemException):
|
||||||
|
|
||||||
|
def __init__(self, title="Unsupported Media Type", detail=None):
|
||||||
|
super(UnsupportedMediaTypeProblem, self).__init__(status=415, title=title, detail=detail)
|
||||||
|
|
||||||
|
|
||||||
class NonConformingResponseBody(NonConformingResponse):
|
class NonConformingResponseBody(NonConformingResponse):
|
||||||
def __init__(self, message, reason="Response body does not conform to specification"):
|
def __init__(self, message, reason="Response body does not conform to specification"):
|
||||||
super(NonConformingResponseBody, self).__init__(reason=reason, message=message)
|
super(NonConformingResponseBody, self).__init__(reason=reason, message=message)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .operations.secure import SecureOperation
|
from .operations.secure import SecureOperation
|
||||||
from .problem import problem
|
from .exceptions import AuthenticationProblem, ResolverProblem
|
||||||
|
|
||||||
logger = logging.getLogger('connexion.handlers')
|
logger = logging.getLogger('connexion.handlers')
|
||||||
|
|
||||||
@@ -45,12 +45,11 @@ class AuthErrorHandler(SecureOperation):
|
|||||||
"""
|
"""
|
||||||
Actual handler for the execution after authentication.
|
Actual handler for the execution after authentication.
|
||||||
"""
|
"""
|
||||||
response = problem(
|
raise AuthenticationProblem(
|
||||||
title=self.exception.name,
|
title=self.exception.name,
|
||||||
detail=self.exception.description,
|
detail=self.exception.description,
|
||||||
status=self.exception.code
|
status=self.exception.code
|
||||||
)
|
)
|
||||||
return self.api.get_response(response)
|
|
||||||
|
|
||||||
|
|
||||||
class ResolverErrorHandler(SecureOperation):
|
class ResolverErrorHandler(SecureOperation):
|
||||||
@@ -68,12 +67,11 @@ class ResolverErrorHandler(SecureOperation):
|
|||||||
return self.handle
|
return self.handle
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
response = problem(
|
raise ResolverProblem(
|
||||||
title='Not Implemented',
|
title='Not Implemented',
|
||||||
detail=self.exception.reason,
|
detail=self.exception.reason,
|
||||||
status=self.status_code
|
status=self.status_code
|
||||||
)
|
)
|
||||||
return self.api.get_response(response)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def operation_id(self):
|
def operation_id(self):
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import functools
|
import functools
|
||||||
import importlib
|
import importlib
|
||||||
import re
|
|
||||||
|
|
||||||
import six
|
import six
|
||||||
import yaml
|
import yaml
|
||||||
|
|||||||
4
requirements-all.txt
Normal file
4
requirements-all.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-r requirements-aiohttp.txt
|
||||||
|
-r requirements-devel.txt
|
||||||
|
openapi_spec_validator
|
||||||
|
pytest-aiohttp
|
||||||
@@ -22,7 +22,7 @@ def aiohttp_app(problem_api_spec_dir):
|
|||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_aiohttp_problems(aiohttp_app, aiohttp_client):
|
def test_aiohttp_problems_404(aiohttp_app, aiohttp_client):
|
||||||
# TODO: This is a based on test_errors.test_errors(). That should be refactored
|
# TODO: This is a based on test_errors.test_errors(). That should be refactored
|
||||||
# so that it is parameterized for all web frameworks.
|
# so that it is parameterized for all web frameworks.
|
||||||
app_client = yield from aiohttp_client(aiohttp_app.app) # type: aiohttp.test_utils.TestClient
|
app_client = yield from aiohttp_client(aiohttp_app.app) # type: aiohttp.test_utils.TestClient
|
||||||
@@ -38,6 +38,12 @@ def test_aiohttp_problems(aiohttp_app, aiohttp_client):
|
|||||||
assert error404['status'] == 404
|
assert error404['status'] == 404
|
||||||
assert 'instance' not in error404
|
assert 'instance' not in error404
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_aiohttp_problems_405(aiohttp_app, aiohttp_client):
|
||||||
|
# TODO: This is a based on test_errors.test_errors(). That should be refactored
|
||||||
|
# so that it is parameterized for all web frameworks.
|
||||||
|
app_client = yield from aiohttp_client(aiohttp_app.app) # type: aiohttp.test_utils.TestClient
|
||||||
|
|
||||||
get_greeting = yield from app_client.get('/v1.0/greeting/jsantos') # type: aiohttp.ClientResponse
|
get_greeting = yield from app_client.get('/v1.0/greeting/jsantos') # type: aiohttp.ClientResponse
|
||||||
assert get_greeting.content_type == 'application/problem+json'
|
assert get_greeting.content_type == 'application/problem+json'
|
||||||
assert get_greeting.status == 405
|
assert get_greeting.status == 405
|
||||||
@@ -49,6 +55,12 @@ def test_aiohttp_problems(aiohttp_app, aiohttp_client):
|
|||||||
assert error405['status'] == 405
|
assert error405['status'] == 405
|
||||||
assert 'instance' not in error405
|
assert 'instance' not in error405
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_aiohttp_problems_500(aiohttp_app, aiohttp_client):
|
||||||
|
# TODO: This is a based on test_errors.test_errors(). That should be refactored
|
||||||
|
# so that it is parameterized for all web frameworks.
|
||||||
|
app_client = yield from aiohttp_client(aiohttp_app.app) # type: aiohttp.test_utils.TestClient
|
||||||
|
|
||||||
get500 = yield from app_client.get('/v1.0/except') # type: aiohttp.ClientResponse
|
get500 = yield from app_client.get('/v1.0/except') # type: aiohttp.ClientResponse
|
||||||
assert get500.content_type == 'application/problem+json'
|
assert get500.content_type == 'application/problem+json'
|
||||||
assert get500.status == 500
|
assert get500.status == 500
|
||||||
@@ -60,6 +72,12 @@ def test_aiohttp_problems(aiohttp_app, aiohttp_client):
|
|||||||
assert error500['status'] == 500
|
assert error500['status'] == 500
|
||||||
assert 'instance' not in error500
|
assert 'instance' not in error500
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_aiohttp_problems_418(aiohttp_app, aiohttp_client):
|
||||||
|
# TODO: This is a based on test_errors.test_errors(). That should be refactored
|
||||||
|
# so that it is parameterized for all web frameworks.
|
||||||
|
app_client = yield from aiohttp_client(aiohttp_app.app) # type: aiohttp.test_utils.TestClient
|
||||||
|
|
||||||
get_problem = yield from app_client.get('/v1.0/problem') # type: aiohttp.ClientResponse
|
get_problem = yield from app_client.get('/v1.0/problem') # type: aiohttp.ClientResponse
|
||||||
assert get_problem.content_type == 'application/problem+json'
|
assert get_problem.content_type == 'application/problem+json'
|
||||||
assert get_problem.status == 418
|
assert get_problem.status == 418
|
||||||
@@ -72,6 +90,12 @@ def test_aiohttp_problems(aiohttp_app, aiohttp_client):
|
|||||||
assert error_problem['status'] == 418
|
assert error_problem['status'] == 418
|
||||||
assert error_problem['instance'] == 'instance1'
|
assert error_problem['instance'] == 'instance1'
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def test_aiohttp_problems_misc(aiohttp_app, aiohttp_client):
|
||||||
|
# TODO: This is a based on test_errors.test_errors(). That should be refactored
|
||||||
|
# so that it is parameterized for all web frameworks.
|
||||||
|
app_client = yield from aiohttp_client(aiohttp_app.app) # type: aiohttp.test_utils.TestClient
|
||||||
|
|
||||||
problematic_json = yield from app_client.get(
|
problematic_json = yield from app_client.get(
|
||||||
'/v1.0/json_response_with_undefined_value_to_serialize') # type: aiohttp.ClientResponse
|
'/v1.0/json_response_with_undefined_value_to_serialize') # type: aiohttp.ClientResponse
|
||||||
assert problematic_json.content_type == 'application/problem+json'
|
assert problematic_json.content_type == 'application/problem+json'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import flask
|
import flask
|
||||||
from flask import jsonify, redirect
|
from flask import jsonify, redirect
|
||||||
|
|
||||||
from connexion import NoContent, ProblemException, context, problem
|
from connexion import NoContent, ProblemException, context
|
||||||
|
|
||||||
|
|
||||||
class DummyClass(object):
|
class DummyClass(object):
|
||||||
@@ -92,19 +92,19 @@ def get_bye_secure_jwt(name, user, token_info):
|
|||||||
return 'Goodbye {name} (Secure: {user})'.format(name=name, user=user)
|
return 'Goodbye {name} (Secure: {user})'.format(name=name, user=user)
|
||||||
|
|
||||||
def with_problem():
|
def with_problem():
|
||||||
return problem(type='http://www.example.com/error',
|
raise ProblemException(type='http://www.example.com/error',
|
||||||
title='Some Error',
|
title='Some Error',
|
||||||
detail='Something went wrong somewhere',
|
detail='Something went wrong somewhere',
|
||||||
status=418,
|
status=418,
|
||||||
instance='instance1',
|
instance='instance1',
|
||||||
headers={'x-Test-Header': 'In Test'})
|
headers={'x-Test-Header': 'In Test'})
|
||||||
|
|
||||||
|
|
||||||
def with_problem_txt():
|
def with_problem_txt():
|
||||||
return problem(title='Some Error',
|
raise ProblemException(title='Some Error',
|
||||||
detail='Something went wrong somewhere',
|
detail='Something went wrong somewhere',
|
||||||
status=418,
|
status=418,
|
||||||
instance='instance1')
|
instance='instance1')
|
||||||
|
|
||||||
|
|
||||||
def internal_error():
|
def internal_error():
|
||||||
@@ -416,8 +416,8 @@ def get_empty_dict():
|
|||||||
|
|
||||||
|
|
||||||
def get_custom_problem_response():
|
def get_custom_problem_response():
|
||||||
return problem(403, "You need to pay", "Missing amount",
|
raise ProblemException(403, "You need to pay", "Missing amount",
|
||||||
ext={'amount': 23.0})
|
ext={'amount': 23.0})
|
||||||
|
|
||||||
|
|
||||||
def throw_problem_exception():
|
def throw_problem_exception():
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
|
import pytest
|
||||||
from mock import MagicMock
|
from mock import MagicMock
|
||||||
|
|
||||||
import connexion
|
import connexion
|
||||||
|
from connexion.exceptions import ProblemException
|
||||||
from connexion.decorators.metrics import UWSGIMetricsCollector
|
from connexion.decorators.metrics import UWSGIMetricsCollector
|
||||||
|
|
||||||
|
|
||||||
@@ -11,13 +13,14 @@ def test_timer(monkeypatch):
|
|||||||
wrapper = UWSGIMetricsCollector('/foo/bar/<param>', 'get')
|
wrapper = UWSGIMetricsCollector('/foo/bar/<param>', 'get')
|
||||||
|
|
||||||
def operation(req):
|
def operation(req):
|
||||||
return connexion.problem(418, '', '')
|
raise ProblemException(418, '', '')
|
||||||
|
|
||||||
op = wrapper(operation)
|
op = wrapper(operation)
|
||||||
metrics = MagicMock()
|
metrics = MagicMock()
|
||||||
monkeypatch.setattr('flask.request', MagicMock())
|
monkeypatch.setattr('flask.request', MagicMock())
|
||||||
monkeypatch.setattr('flask.current_app', MagicMock(response_class=flask.Response))
|
monkeypatch.setattr('flask.current_app', MagicMock(response_class=flask.Response))
|
||||||
monkeypatch.setattr('connexion.decorators.metrics.uwsgi_metrics', metrics)
|
monkeypatch.setattr('connexion.decorators.metrics.uwsgi_metrics', metrics)
|
||||||
op(MagicMock())
|
with pytest.raises(ProblemException) as exc:
|
||||||
|
op(MagicMock())
|
||||||
assert metrics.timer.call_args[0][:2] == ('connexion.response',
|
assert metrics.timer.call_args[0][:2] == ('connexion.response',
|
||||||
'418.GET.foo.bar.{param}')
|
'418.GET.foo.bar.{param}')
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
|
import pytest
|
||||||
# we are using "mock" module here for Py 2.7 support
|
# we are using "mock" module here for Py 2.7 support
|
||||||
from mock import MagicMock
|
from mock import MagicMock
|
||||||
|
|
||||||
from connexion.apis.flask_api import FlaskApi
|
from connexion.apis.flask_api import FlaskApi
|
||||||
|
from connexion.exceptions import BadRequestProblem
|
||||||
from connexion.decorators.validation import ParameterValidator
|
from connexion.decorators.validation import ParameterValidator
|
||||||
|
|
||||||
|
|
||||||
@@ -34,40 +36,61 @@ def test_parameter_validator(monkeypatch):
|
|||||||
|
|
||||||
kwargs = {'query': {}, 'headers': {}, 'cookies': {}}
|
kwargs = {'query': {}, 'headers': {}, 'cookies': {}}
|
||||||
request = MagicMock(path_params={}, **kwargs)
|
request = MagicMock(path_params={}, **kwargs)
|
||||||
assert json.loads(handler(request).data.decode())['detail'] == "Missing path parameter 'p1'"
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
|
handler(request)
|
||||||
|
assert exc.value.detail == "Missing path parameter 'p1'"
|
||||||
request = MagicMock(path_params={'p1': '123'}, **kwargs)
|
request = MagicMock(path_params={'p1': '123'}, **kwargs)
|
||||||
assert handler(request) == 'OK'
|
assert handler(request) == 'OK'
|
||||||
request = MagicMock(path_params={'p1': ''}, **kwargs)
|
request = MagicMock(path_params={'p1': ''}, **kwargs)
|
||||||
assert json.loads(handler(request).data.decode())['detail'] == "Wrong type, expected 'integer' for path parameter 'p1'"
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
|
handler(request)
|
||||||
|
assert exc.value.detail == "Wrong type, expected 'integer' for path parameter 'p1'"
|
||||||
request = MagicMock(path_params={'p1': 'foo'}, **kwargs)
|
request = MagicMock(path_params={'p1': 'foo'}, **kwargs)
|
||||||
assert json.loads(handler(request).data.decode())['detail'] == "Wrong type, expected 'integer' for path parameter 'p1'"
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
|
handler(request)
|
||||||
|
assert exc.value.detail == "Wrong type, expected 'integer' for path parameter 'p1'"
|
||||||
request = MagicMock(path_params={'p1': '1.2'}, **kwargs)
|
request = MagicMock(path_params={'p1': '1.2'}, **kwargs)
|
||||||
assert json.loads(handler(request).data.decode())['detail'] == "Wrong type, expected 'integer' for path parameter 'p1'"
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
|
handler(request)
|
||||||
|
assert exc.value.detail == "Wrong type, expected 'integer' for path parameter 'p1'"
|
||||||
|
|
||||||
request = MagicMock(path_params={'p1': 1}, query={'q1': '4'}, headers={}, cookies={})
|
request = MagicMock(path_params={'p1': 1}, query={'q1': '4'}, headers={})
|
||||||
assert json.loads(handler(request).data.decode())['detail'].startswith('4 is greater than the maximum of 3')
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
|
handler(request)
|
||||||
|
assert exc.value.detail.startswith('4 is greater than the maximum of 3')
|
||||||
request = MagicMock(path_params={'p1': 1}, query={'q1': '3'}, headers={}, cookies={})
|
request = MagicMock(path_params={'p1': 1}, query={'q1': '3'}, headers={}, cookies={})
|
||||||
assert handler(request) == 'OK'
|
assert handler(request) == 'OK'
|
||||||
|
|
||||||
request = MagicMock(path_params={'p1': 1}, query={'a1': ['1', '2']}, headers={}, cookies={})
|
request = MagicMock(path_params={'p1': 1}, query={'a1': ['1', '2']}, headers={}, cookies={})
|
||||||
assert handler(request) == "OK"
|
assert handler(request) == "OK"
|
||||||
request = MagicMock(path_params={'p1': 1}, query={'a1': ['1', 'a']}, headers={}, cookies={})
|
request = MagicMock(path_params={'p1': 1}, query={'a1': ['1', 'a']}, headers={})
|
||||||
assert json.loads(handler(request).data.decode())['detail'].startswith("'a' is not of type 'integer'")
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
request = MagicMock(path_params={'p1': 1}, query={'a1': ['1', '-1']}, headers={}, cookies={})
|
handler(request)
|
||||||
assert json.loads(handler(request).data.decode())['detail'].startswith("-1 is less than the minimum of 0")
|
assert exc.value.detail.startswith("'a' is not of type 'integer'")
|
||||||
request = MagicMock(path_params={'p1': 1}, query={'a1': ['1']}, headers={}, cookies={})
|
|
||||||
assert json.loads(handler(request).data.decode())['detail'].startswith("[1] is too short")
|
|
||||||
request = MagicMock(path_params={'p1': 1}, query={'a1': ['1', '2', '3', '4']}, headers={}, cookies={})
|
|
||||||
assert json.loads(handler(request).data.decode())['detail'].startswith("[1, 2, 3, 4] is too long")
|
|
||||||
|
|
||||||
request = MagicMock(path_params={'p1': '123'}, query={}, headers={'h1': 'a'}, cookies={})
|
|
||||||
assert handler(request) == 'OK'
|
|
||||||
|
|
||||||
request = MagicMock(path_params={'p1': '123'}, query={}, headers={'h1': 'x'}, cookies={})
|
|
||||||
assert json.loads(handler(request).data.decode())['detail'].startswith("'x' is not one of ['a', 'b']")
|
|
||||||
|
|
||||||
request = MagicMock(path_params={'p1': '123'}, query={}, headers={}, cookies={'c1': 'b'})
|
request = MagicMock(path_params={'p1': '123'}, query={}, headers={}, cookies={'c1': 'b'})
|
||||||
assert handler(request) == 'OK'
|
assert handler(request) == 'OK'
|
||||||
|
|
||||||
request = MagicMock(path_params={'p1': '123'}, query={}, headers={}, cookies={'c1': 'x'})
|
request = MagicMock(path_params={'p1': '123'}, query={}, headers={}, cookies={'c1': 'x'})
|
||||||
assert json.loads(handler(request).data.decode())['detail'].startswith("'x' is not one of ['a', 'b']")
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
|
assert handler(request)
|
||||||
|
assert exc.value.detail.startswith("'x' is not one of ['a', 'b']")
|
||||||
|
request = MagicMock(path_params={'p1': 1}, query={'a1': ['1', '-1']}, headers={})
|
||||||
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
|
handler(request)
|
||||||
|
assert exc.value.detail.startswith("-1 is less than the minimum of 0")
|
||||||
|
request = MagicMock(path_params={'p1': 1}, query={'a1': ['1']}, headers={})
|
||||||
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
|
handler(request)
|
||||||
|
assert exc.value.detail.startswith("[1] is too short")
|
||||||
|
request = MagicMock(path_params={'p1': 1}, query={'a1': ['1', '2', '3', '4']}, headers={})
|
||||||
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
|
handler(request)
|
||||||
|
assert exc.value.detail.startswith("[1, 2, 3, 4] is too long")
|
||||||
|
|
||||||
|
request = MagicMock(path_params={'p1': '123'}, query={}, headers={'h1': 'a'}, cookies={})
|
||||||
|
assert handler(request) == 'OK'
|
||||||
|
|
||||||
|
request = MagicMock(path_params={'p1': '123'}, query={}, headers={'h1': 'x'})
|
||||||
|
with pytest.raises(BadRequestProblem) as exc:
|
||||||
|
handler(request)
|
||||||
|
assert exc.value.detail.startswith("'x' is not one of ['a', 'b']")
|
||||||
|
|||||||
Reference in New Issue
Block a user