mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-06 12:27:45 +00:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
16
tests/fixtures/secure_endpoint/openapi.yaml
vendored
16
tests/fixtures/secure_endpoint/openapi.yaml
vendored
@@ -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
|
||||
|
||||
17
tests/fixtures/secure_endpoint/swagger.yaml
vendored
17
tests/fixtures/secure_endpoint/swagger.yaml
vendored
@@ -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
|
||||
|
||||
17
tests/fixtures/simple/openapi.yaml
vendored
17
tests/fixtures/simple/openapi.yaml
vendored
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user