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:
Brian Price
2019-10-24 03:59:05 -05:00
committed by Henning Jacobs
parent c8d8973c7e
commit b0b83c4879
14 changed files with 157 additions and 88 deletions

View File

@@ -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:

View File

@@ -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):

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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):

View File

@@ -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
View File

@@ -0,0 +1,4 @@
-r requirements-aiohttp.txt
-r requirements-devel.txt
openapi_spec_validator
pytest-aiohttp

View File

@@ -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'

View File

@@ -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():

View File

@@ -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}')

View File

@@ -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']")