Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge V2 to main #1518

Merged
merged 6 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions connexion/decorators/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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)

Expand Down
38 changes: 20 additions & 18 deletions connexion/operations/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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")
Expand Down Expand Up @@ -281,13 +282,28 @@ def _get_body_argument(self, body, arguments, has_kwargs, sanitize):
'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
Expand All @@ -297,25 +313,11 @@ def _get_body_argument(self, body, arguments, has_kwargs, sanitize):
# 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):
Expand Down
31 changes: 31 additions & 0 deletions tests/api/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions tests/api/test_secure_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
7 changes: 6 additions & 1 deletion tests/decorators/test_parameter.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -25,3 +25,8 @@ def get_arguments(self, *args, **kwargs):

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'
7 changes: 6 additions & 1 deletion tests/fakeapi/auth.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
8 changes: 8 additions & 0 deletions tests/fakeapi/hello/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import uuid

from connexion import NoContent, ProblemException, context, request
from connexion.exceptions import OAuthProblem
from flask import jsonify, redirect, send_file


Expand Down Expand Up @@ -463,6 +464,9 @@ def optional_auth(**kwargs):
return "Authenticated"


def auth_exception():
return 'foo'

def test_args_kwargs(*args, **kwargs):
return kwargs

Expand Down Expand Up @@ -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 ""

Expand Down
16 changes: 16 additions & 0 deletions tests/fixtures/secure_endpoint/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 changes: 17 additions & 0 deletions tests/fixtures/secure_endpoint/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 changes: 17 additions & 0 deletions tests/fixtures/simple/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down