Merge V2 to main (#1518)

* Fix uri parsing for query parameter with empty brackets (#1501)

* Update tests for changed werkzeug behavior in 2.1 (#1506)

https://github.com/pallets/werkzeug/issues/2352

* Bugfix/async security check (#1512)

* Add failing tests

* Use for else construct

* openapi: remove JSON body second validation and type casting (#1170)

* openapi: remove body preprocessing

Body is already validated using jsonschema. There was also some type
casting but it was wrong: e.g. not recurring deeply into dicts and lists,
relying on existence of "type" in schema (which is not there e.g. if
oneOf is used). Anyway, the only reason why types should be casted is
converting integer values to float if the type is number. But this is in
most cases irrelevant.

Added an example, which did not work before this commit (echoed `{}`)
e.g. for
```
curl localhost:8080/api/foo -H 'content-type: application/json' -d
'{"foo": 1}'
```
but now the example works (echoes `{"foo": 1}`).

* test with oneOf in the requestBody

* remove oneof examples: superseded by tests

Co-authored-by: Pavol Vargovcik <pavol.vargovcik@kiwi.com>

Co-authored-by: Ruwann <ruwanlambrichts@gmail.com>
Co-authored-by: Pavol Vargovčík <pavol.vargovcik@gmail.com>
Co-authored-by: Pavol Vargovcik <pavol.vargovcik@kiwi.com>
This commit is contained in:
Robbe Sneyders
2022-05-04 17:31:04 +02:00
committed by GitHub
parent 972d1a9f9c
commit 156bf79ee6
10 changed files with 136 additions and 27 deletions

View File

@@ -48,6 +48,17 @@ def snake_and_shadow(name):
return snake
def sanitized(name):
return name and re.sub('^[^a-zA-Z_]+', '',
re.sub('[^0-9a-zA-Z_]', '',
re.sub(r'\[(?!])', '_', name)))
def pythonic(name):
name = name and snake_and_shadow(name)
return sanitized(name)
def parameter_to_arg(operation, function, pythonic_params=False,
pass_context_arg_name=None):
"""
@@ -65,13 +76,6 @@ def parameter_to_arg(operation, function, pythonic_params=False,
"""
consumes = operation.consumes
def sanitized(name):
return name and re.sub('^[^a-zA-Z_]+', '', re.sub('[^0-9a-zA-Z[_]', '', re.sub(r'[\[]', '_', name)))
def pythonic(name):
name = name and snake_and_shadow(name)
return sanitized(name)
sanitize = pythonic if pythonic_params else sanitized
arguments, has_kwargs = inspect_function_arguments(function)

View File

@@ -9,6 +9,7 @@ from copy import copy, deepcopy
from connexion.operations.abstract import AbstractOperation
from ..decorators.uri_parsing import OpenAPIURIParser
from ..http_facts import FORM_CONTENT_TYPES
from ..utils import deep_get, deep_merge, is_null, is_nullable, make_type
logger = logging.getLogger("connexion.operations.openapi3")
@@ -281,13 +282,28 @@ class OpenAPIOperation(AbstractOperation):
'the requestBody instead.', DeprecationWarning)
x_body_name = sanitize(self.body_schema.get('x-body-name', 'body'))
if self.consumes[0] in FORM_CONTENT_TYPES:
result = self._get_body_argument_form(body)
else:
result = self._get_body_argument_json(body)
if x_body_name in arguments or has_kwargs:
return {x_body_name: result}
return {}
def _get_body_argument_json(self, body):
# if the body came in null, and the schema says it can be null, we decide
# to include no value for the body argument, rather than the default body
if is_nullable(self.body_schema) and is_null(body):
if x_body_name in arguments or has_kwargs:
return {x_body_name: None}
return {}
return None
if body is None:
default_body = self.body_schema.get('default', {})
return deepcopy(default_body)
return body
def _get_body_argument_form(self, body):
# now determine the actual value for the body (whether it came in or is default)
default_body = self.body_schema.get('default', {})
body_props = {k: {"schema": v} for k, v
@@ -297,25 +313,11 @@ class OpenAPIOperation(AbstractOperation):
# see: https://github.com/OAI/OpenAPI-Specification/blame/3.0.2/versions/3.0.2.md#L2305
additional_props = self.body_schema.get("additionalProperties", True)
if body is None:
body = deepcopy(default_body)
# if the body isn't even an object, then none of the concerns below matter
if self.body_schema.get("type") != "object":
if x_body_name in arguments or has_kwargs:
return {x_body_name: body}
return {}
# supply the initial defaults and convert all values to the proper types by schema
body_arg = deepcopy(default_body)
body_arg.update(body or {})
res = {}
if body_props or additional_props:
res = self._get_typed_body_values(body_arg, body_props, additional_props)
if x_body_name in arguments or has_kwargs:
return {x_body_name: res}
return self._get_typed_body_values(body_arg, body_props, additional_props)
return {}
def _get_typed_body_values(self, body_arg, body_props, additional_props):

View File

@@ -388,3 +388,34 @@ def test_streaming_response(simple_app):
app_client = simple_app.app.test_client()
resp = app_client.get('/v1.0/get_streaming_response')
assert resp.status_code == 200
def test_oneof(simple_openapi_app):
app_client = simple_openapi_app.app.test_client()
post_greeting = app_client.post( # type: flask.Response
'/v1.0/oneof_greeting',
data=json.dumps({"name": 3}),
content_type="application/json"
)
assert post_greeting.status_code == 200
assert post_greeting.content_type == 'application/json'
greeting_reponse = json.loads(post_greeting.data.decode('utf-8', 'replace'))
assert greeting_reponse['greeting'] == 'Hello 3'
post_greeting = app_client.post( # type: flask.Response
'/v1.0/oneof_greeting',
data=json.dumps({"name": True}),
content_type="application/json"
)
assert post_greeting.status_code == 200
assert post_greeting.content_type == 'application/json'
greeting_reponse = json.loads(post_greeting.data.decode('utf-8', 'replace'))
assert greeting_reponse['greeting'] == 'Hello True'
post_greeting = app_client.post( # type: flask.Response
'/v1.0/oneof_greeting',
data=json.dumps({"name": "jsantos"}),
content_type="application/json"
)
assert post_greeting.status_code == 400

View File

@@ -96,6 +96,10 @@ def test_security(oauth_requests, secure_endpoint_app):
assert response.data == b'"Unauthenticated"\n'
assert response.status_code == 200
# security function throws exception
response = app_client.get('/v1.0/auth-exception', headers={'X-Api-Key': 'foo'})
assert response.status_code == 401
def test_checking_that_client_token_has_all_necessary_scopes(
oauth_requests, secure_endpoint_app):

View File

@@ -1,6 +1,6 @@
from unittest.mock import MagicMock
from connexion.decorators.parameter import parameter_to_arg
from connexion.decorators.parameter import parameter_to_arg, pythonic
def test_injection():
@@ -25,3 +25,8 @@ def test_injection():
parameter_to_arg(Op(), handler, pass_context_arg_name='framework_request_ctx')(request)
func.assert_called_with(p1='123', framework_request_ctx=request.context)
def test_pythonic_params():
assert pythonic('orderBy[eq]') == 'order_by_eq'
assert pythonic('ids[]') == 'ids'

View File

@@ -1,6 +1,7 @@
import asyncio
import json
from connexion.exceptions import OAuthProblem
def fake_basic_auth(username, password, required_scopes=None):
if username == password:
@@ -13,3 +14,7 @@ def fake_json_auth(token, required_scopes=None):
return json.loads(token)
except ValueError:
return None
async def async_auth_exception(token, required_scopes=None, request=None):
raise OAuthProblem

View File

@@ -3,6 +3,7 @@ import datetime
import uuid
from connexion import NoContent, ProblemException, context, request
from connexion.exceptions import OAuthProblem
from flask import jsonify, redirect, send_file
@@ -463,6 +464,9 @@ def optional_auth(**kwargs):
return "Authenticated"
def auth_exception():
return 'foo'
def test_args_kwargs(*args, **kwargs):
return kwargs
@@ -569,6 +573,10 @@ def jwt_info(token):
return None
def apikey_exception(token):
raise OAuthProblem()
def get_add_operation_on_http_methods_only():
return ""

View File

@@ -138,6 +138,17 @@ paths:
responses:
'200':
description: some response
/auth-exception:
get:
summary: Test security handler function that raises an exception
description: Throw error from security function
operationId: fakeapi.hello.auth_exception
security:
- auth_exception: []
responses:
'200':
description: some response
servers:
- url: /v1.0
components:
@@ -161,3 +172,8 @@ components:
scheme: bearer
bearerFormat: JWT
x-bearerInfoFunc: fakeapi.hello.jwt_info
auth_exception:
type: apiKey
name: X-Api-Key
in: header
x-apikeyInfoFunc: fakeapi.hello.apikey_exception

View File

@@ -29,6 +29,12 @@ securityDefinitions:
x-authentication-scheme: Bearer
x-bearerInfoFunc: fakeapi.hello.jwt_info
auth_exception:
type: apiKey
name: X-Api-Key
in: header
x-apikeyInfoFunc: fakeapi.hello.apikey_exception
paths:
/byesecure/{name}:
get:
@@ -171,3 +177,14 @@ paths:
responses:
'200':
description: some response
/auth-exception:
get:
summary: Test security handler function that raises an exception
description: Throw error from security function
operationId: fakeapi.hello.auth_exception
security:
- auth_exception: []
responses:
'200':
description: some response

View File

@@ -1222,6 +1222,23 @@ paths:
schema:
type: string
format: binary
/oneof_greeting:
post:
operationId: fakeapi.hello.post_greeting3
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
oneOf:
- {type: boolean}
- {type: number}
additionalProperties: false
responses:
'200':
description: Echo the validated request.
servers:
- url: http://localhost:{port}/{basePath}