Skip to content

Commit

Permalink
Merge branch 'master' into fix_bodyvalidator_formdata_refed
Browse files Browse the repository at this point in the history
  • Loading branch information
positron96 committed Aug 14, 2018
2 parents 5c8698e + ccd62f8 commit 85f1f7d
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 38 deletions.
2 changes: 0 additions & 2 deletions .zappr.yaml
Expand Up @@ -10,5 +10,3 @@ approvals:
# a public zalando org member
# (any org in here counts)
- zalando
# OR a collaborator of the repo
collaborators: true
27 changes: 16 additions & 11 deletions connexion/decorators/uri_parsing.py
Expand Up @@ -12,6 +12,8 @@

@six.add_metaclass(abc.ABCMeta)
class AbstractURIParser(BaseDecorator):
parsable_parameters = ["query", "path"]

def __init__(self, param_defns):
"""
a URI parser is initialized with parameter definitions.
Expand All @@ -25,7 +27,7 @@ def __init__(self, param_defns):
"""
self._param_defns = {p["name"]: p
for p in param_defns
if p["in"] in ["query", "path"]}
if p["in"] in self.parsable_parameters}

@abc.abstractproperty
def param_defns(self):
Expand Down Expand Up @@ -97,18 +99,20 @@ def __call__(self, function):

@functools.wraps(function)
def wrapper(request):

try:
query = request.query.to_dict(flat=False)
except AttributeError:
query = dict(request.query.items())

try:
path_params = request.path_params.to_dict(flat=False)
except AttributeError:
path_params = dict(request.path_params.items())
def coerce_dict(md):
""" MultiDict -> dict of lists
"""
try:
return md.to_dict(flat=False)
except AttributeError:
return dict(md.items())

query = coerce_dict(request.query)
path_params = coerce_dict(request.path_params)
form = coerce_dict(request.form)

request.query = self.resolve_params(query, resolve_duplicates=True)
request.form = self.resolve_params(form, resolve_duplicates=True)
request.path_params = self.resolve_params(path_params)
response = function(request)
return response
Expand All @@ -121,6 +125,7 @@ class Swagger2URIParser(AbstractURIParser):
Adheres to the Swagger2 spec,
Assumes the the last defined query parameter should be used.
"""
parsable_parameters = ["query", "path", "formData"]

@property
def param_defns(self):
Expand Down
20 changes: 2 additions & 18 deletions connexion/operation.py
Expand Up @@ -2,9 +2,6 @@
import logging
from copy import deepcopy

from jsonschema import ValidationError

from .decorators import validation
from .decorators.decorator import (BeginOfRequestLifecycleDecorator,
EndOfRequestLifecycleDecorator)
from .decorators.metrics import UWSGIMetricsCollector
Expand All @@ -15,8 +12,7 @@
security_passthrough, verify_oauth_local,
verify_oauth_remote)
from .decorators.uri_parsing import AlwaysMultiURIParser
from .decorators.validation import (ParameterValidator, RequestBodyValidator,
TypeValidationError)
from .decorators.validation import ParameterValidator, RequestBodyValidator
from .exceptions import InvalidSpecification
from .utils import all_json, is_nullable

Expand Down Expand Up @@ -233,18 +229,6 @@ def __init__(self, api, method, path, operation, resolver, app_produces, app_con
self.operation_id = resolution.operation_id
self.__undecorated_function = resolution.function

self.validate_defaults()

def validate_defaults(self):
for param in self.parameters:
try:
if param['in'] == 'query' and 'default' in param:
validation.validate_type(param, param['default'], 'query', param['name'])
except (TypeValidationError, ValidationError):
raise InvalidSpecification('The parameter \'{param_name}\' has a default value which is not of'
' type \'{param_type}\''.format(param_name=param['name'],
param_type=param['type']))

def resolve_reference(self, schema):
schema = deepcopy(schema) # avoid changing the original schema
self.check_references(schema)
Expand Down Expand Up @@ -400,7 +384,7 @@ def function(self):
function = validation_decorator(function)

uri_parsing_decorator = self.__uri_parsing_decorator
logging.debug('... Adding uri parsing decorator (%r)', uri_parsing_decorator)
logger.debug('... Adding uri parsing decorator (%r)', uri_parsing_decorator)
function = uri_parsing_decorator(function)

# NOTE: the security decorator should be applied last to check auth before anything else :-)
Expand Down
3 changes: 2 additions & 1 deletion docs/routing.rst
Expand Up @@ -88,7 +88,8 @@ under-score is encountered. As an example:
'top'
Without this sanitation it would e.g. be impossible to implement an
[OData](http://www.odata.org) API.
`OData
<http://www.odata.org>`_ API.

Parameter Variable Converters
-----------------------------
Expand Down
18 changes: 18 additions & 0 deletions tests/api/test_bootstrap.py
Expand Up @@ -4,6 +4,7 @@
import yaml
from swagger_spec_validator.common import SwaggerValidationError

import mock
import pytest
from conftest import TEST_FOLDER, build_app_from_fixture
from connexion import App
Expand Down Expand Up @@ -187,3 +188,20 @@ def test_default_query_param_does_not_match_defined_type(
default_param_error_spec_dir):
with pytest.raises(SwaggerValidationError):
build_app_from_fixture(default_param_error_spec_dir, validate_responses=True, debug=False)


def test_handle_add_operation_error_debug(simple_api_spec_dir):
app = App(__name__, specification_dir=simple_api_spec_dir, debug=True)
app.api_cls = type('AppTest', (app.api_cls,), {})
app.api_cls.add_operation = mock.MagicMock(side_effect=Exception('operation error!'))
api = app.add_api('swagger.yaml', resolver=lambda oid: (lambda foo: 'bar'))
assert app.api_cls.add_operation.called
assert api.resolver.resolve_function_from_operation_id('faux')('bah') == 'bar'


def test_handle_add_operation_error(simple_api_spec_dir):
app = App(__name__, specification_dir=simple_api_spec_dir)
app.api_cls = type('AppTest', (app.api_cls,), {})
app.api_cls.add_operation = mock.MagicMock(side_effect=Exception('operation error!'))
with pytest.raises(Exception):
app.add_api('swagger.yaml', resolver=lambda oid: (lambda foo: 'bar'))
27 changes: 27 additions & 0 deletions tests/api/test_parameters.py
Expand Up @@ -68,6 +68,33 @@ def test_array_query_param(simple_app):
assert array_response == [4, 5, 6, 7, 8, 9]


def test_array_form_param(simple_app):
app_client = simple_app.app.test_client()
headers = {'Content-type': 'application/x-www-form-urlencoded'}
url = '/v1.0/test_array_csv_form_param'
response = app_client.post(url, headers=headers)
array_response = json.loads(response.data.decode('utf-8', 'replace')) # type: [str]
assert array_response == ['squash', 'banana']
url = '/v1.0/test_array_csv_form_param'
response = app_client.post(url, headers=headers, data={"items": "one,two,three"})
array_response = json.loads(response.data.decode('utf-8', 'replace')) # type: [str]
assert array_response == ['one', 'two', 'three']
url = '/v1.0/test_array_pipes_form_param'
response = app_client.post(url, headers=headers, data={"items": "1|2|3"})
array_response = json.loads(response.data.decode('utf-8', 'replace')) # type: [int]
assert array_response == [1, 2, 3]
url = '/v1.0/test_array_csv_form_param'
data = 'items=A&items=B&items=C&items=D,E,F'
response = app_client.post(url, headers=headers, data=data)
array_response = json.loads(response.data.decode('utf-8', 'replace')) # type: [str] multi array with csv format
assert array_response == ['A', 'B', 'C', 'D', 'E', 'F']
url = '/v1.0/test_array_pipes_form_param'
data = 'items=4&items=5&items=6&items=7|8|9'
response = app_client.post(url, headers=headers, data=data)
array_response = json.loads(response.data.decode('utf-8', 'replace')) # type: [int] multi array with pipes format
assert array_response == [4, 5, 6, 7, 8, 9]


def test_extra_query_param(simple_app):
app_client = simple_app.app.test_client()
headers = {'Content-type': 'application/json'}
Expand Down
47 changes: 41 additions & 6 deletions tests/decorators/test_uri_parsing.py
@@ -1,12 +1,16 @@
from werkzeug.datastructures import MultiDict

import pytest
from connexion.decorators.uri_parsing import (AlwaysMultiURIParser,
FirstValueURIParser,
Swagger2URIParser)

QUERY1 = ["a", "b,c", "d,e,f"]
QUERY2 = ["a", "b|c", "d|e|f"]
PATH1 = "d,e,f"
PATH2 = "d|e|f"
QUERY1 = MultiDict([("letters", "a"), ("letters", "b,c"),
("letters", "d,e,f")])
QUERY2 = MultiDict([("letters", "a"), ("letters", "b|c"),
("letters", "d|e|f")])
PATH1 = {"letters": "d,e,f"}
PATH2 = {"letters": "d|e|f"}
CSV = "csv"
PIPES = "pipes"
MULTI = "multi"
Expand All @@ -24,8 +28,9 @@
(AlwaysMultiURIParser, ['a', 'b', 'c', 'd', 'e', 'f'], QUERY2, PIPES)])
def test_uri_parser_query_params(parser_class, expected, query_in, collection_format):
class Request(object):
query = {"letters": query_in}
query = query_in
path_params = {}
form = {}

request = Request()
parameters = [
Expand All @@ -40,6 +45,35 @@ class Request(object):
assert res.query["letters"] == expected


@pytest.mark.parametrize("parser_class, expected, query_in, collection_format", [
(Swagger2URIParser, ['d', 'e', 'f'], QUERY1, CSV),
(FirstValueURIParser, ['a'], QUERY1, CSV),
(AlwaysMultiURIParser, ['a', 'b', 'c', 'd', 'e', 'f'], QUERY1, CSV),
(Swagger2URIParser, ['a', 'b', 'c', 'd', 'e', 'f'], QUERY1, MULTI),
(FirstValueURIParser, ['a', 'b', 'c', 'd', 'e', 'f'], QUERY1, MULTI),
(AlwaysMultiURIParser, ['a', 'b', 'c', 'd', 'e', 'f'], QUERY1, MULTI),
(Swagger2URIParser, ['d', 'e', 'f'], QUERY2, PIPES),
(FirstValueURIParser, ['a'], QUERY2, PIPES),
(AlwaysMultiURIParser, ['a', 'b', 'c', 'd', 'e', 'f'], QUERY2, PIPES)])
def test_uri_parser_form_params(parser_class, expected, query_in, collection_format):
class Request(object):
query = {}
form = query_in
path_params = {}

request = Request()
parameters = [
{"name": "letters",
"in": "formData",
"type": "array",
"items": {"type": "string"},
"collectionFormat": collection_format}
]
p = parser_class(parameters)
res = p(lambda x: x)(request)
assert res.form["letters"] == expected


@pytest.mark.parametrize("parser_class, expected, query_in, collection_format", [
(Swagger2URIParser, ['d', 'e', 'f'], PATH1, CSV),
(FirstValueURIParser, ['d', 'e', 'f'], PATH1, CSV),
Expand All @@ -50,7 +84,8 @@ class Request(object):
def test_uri_parser_path_params(parser_class, expected, query_in, collection_format):
class Request(object):
query = {}
path_params = {"letters": query_in}
form = {}
path_params = query_in

request = Request()
parameters = [
Expand Down
8 changes: 8 additions & 0 deletions tests/fakeapi/hello.py
Expand Up @@ -210,6 +210,14 @@ def test_array_csv_query_param(items):
return items


def test_array_pipes_form_param(items):
return items


def test_array_csv_form_param(items):
return items


def test_array_pipes_query_param(items):
return items

Expand Down
31 changes: 31 additions & 0 deletions tests/fixtures/simple/swagger.yaml
Expand Up @@ -244,6 +244,37 @@ paths:
200:
description: OK

/test_array_csv_form_param:
post:
operationId: fakeapi.hello.test_array_csv_form_param
parameters:
- name: items
in: formData
description: An comma separated array of items
type: array
items:
type: string
collectionFormat: csv
default: ["squash", "banana"]
responses:
200:
description: OK

/test_array_pipes_form_param:
post:
operationId: fakeapi.hello.test_array_pipes_form_param
parameters:
- name: items
in: formData
description: An comma separated array of items
type: array
items:
type: integer
collectionFormat: pipes
responses:
200:
description: OK

/test_array_csv_query_param:
get:
operationId: fakeapi.hello.test_array_csv_query_param
Expand Down

0 comments on commit 85f1f7d

Please sign in to comment.