diff --git a/openapi_core/contrib/starlette/requests.py b/openapi_core/contrib/starlette/requests.py index 2eebc99b..d31886bc 100644 --- a/openapi_core/contrib/starlette/requests.py +++ b/openapi_core/contrib/starlette/requests.py @@ -45,7 +45,7 @@ def body(self) -> Optional[str]: @property def mimetype(self) -> str: - content_type = self.request.headers["Content-Type"] + content_type = self.request.headers.get("Content-Type") if content_type: return content_type.partition(";")[0] diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 259f09c1..7d4db1f7 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,3 +1,4 @@ +from base64 import b64decode from os import path from urllib import request @@ -25,6 +26,19 @@ def spec_from_url(base_uri): return Spec.from_dict(spec_dict, base_uri=base_uri) +@pytest.fixture(scope="session") +def data_gif(): + return b64decode( + """ +R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d +3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA +AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg +EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD +Fzk0lpcjIQA7 +""" + ) + + class Factory(dict): __getattr__ = dict.__getitem__ __setattr__ = dict.__setitem__ diff --git a/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/__init__.py b/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/__main__.py b/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/__main__.py new file mode 100644 index 00000000..13109d64 --- /dev/null +++ b/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/__main__.py @@ -0,0 +1,15 @@ +from aiohttp import web +from aiohttpproject.pets.views import PetPhotoView + +routes = [ + web.view("/v1/pets/{petId}/photo", PetPhotoView), +] + + +def get_app(loop=None): + app = web.Application(loop=loop) + app.add_routes(routes) + return app + + +app = get_app() diff --git a/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/openapi.py b/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/openapi.py new file mode 100644 index 00000000..74119263 --- /dev/null +++ b/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/openapi.py @@ -0,0 +1,9 @@ +from pathlib import Path + +import yaml + +from openapi_core import Spec + +openapi_spec_path = Path("tests/integration/data/v3.0/petstore.yaml") +spec_dict = yaml.load(openapi_spec_path.read_text(), yaml.Loader) +spec = Spec.from_dict(spec_dict) diff --git a/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/pets/__init__.py b/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/pets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/pets/views.py b/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/pets/views.py new file mode 100644 index 00000000..fea3545e --- /dev/null +++ b/tests/integration/contrib/aiohttp/data/v3.0/aiohttpproject/pets/views.py @@ -0,0 +1,52 @@ +from base64 import b64decode +from io import BytesIO + +from aiohttp import web +from aiohttpproject.openapi import spec +from multidict import MultiDict + +from openapi_core import unmarshal_request +from openapi_core import unmarshal_response +from openapi_core.contrib.aiohttp import AIOHTTPOpenAPIWebRequest +from openapi_core.contrib.aiohttp import AIOHTTPOpenAPIWebResponse + + +class PetPhotoView(web.View): + OPENID_LOGO = b64decode( + """ +R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d +3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA +AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg +EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD +Fzk0lpcjIQA7 +""" + ) + + async def get(self): + request_body = await self.request.text() + openapi_request = AIOHTTPOpenAPIWebRequest( + self.request, body=request_body + ) + request_unmarshalled = unmarshal_request(openapi_request, spec=spec) + response = web.Response( + body=self.OPENID_LOGO, + content_type="image/gif", + ) + openapi_response = AIOHTTPOpenAPIWebResponse(response) + response_unmarshalled = unmarshal_response( + openapi_request, openapi_response, spec=spec + ) + return response + + async def post(self): + request_body = await self.request.read() + openapi_request = AIOHTTPOpenAPIWebRequest( + self.request, body=request_body + ) + request_unmarshalled = unmarshal_request(openapi_request, spec=spec) + response = web.Response(status=201) + openapi_response = AIOHTTPOpenAPIWebResponse(response) + response_unmarshalled = unmarshal_response( + openapi_request, openapi_response, spec=spec + ) + return response diff --git a/tests/integration/contrib/aiohttp/test_aiohttp_project.py b/tests/integration/contrib/aiohttp/test_aiohttp_project.py new file mode 100644 index 00000000..f7abfee3 --- /dev/null +++ b/tests/integration/contrib/aiohttp/test_aiohttp_project.py @@ -0,0 +1,83 @@ +import os +import sys +from base64 import b64encode + +import pytest +from starlette.testclient import TestClient + + +@pytest.fixture(autouse=True, scope="session") +def project_setup(): + directory = os.path.abspath(os.path.dirname(__file__)) + project_dir = os.path.join(directory, "data/v3.0") + sys.path.insert(0, project_dir) + yield + sys.path.remove(project_dir) + + +@pytest.fixture +def app(project_setup, loop): + from aiohttpproject.__main__ import get_app + + return get_app(loop=loop) + + +@pytest.fixture +async def client(app, aiohttp_client): + return await aiohttp_client(app) + + +class BaseTestPetstore: + api_key = "12345" + + @property + def api_key_encoded(self): + api_key_bytes = self.api_key.encode("utf8") + api_key_bytes_enc = b64encode(api_key_bytes) + return str(api_key_bytes_enc, "utf8") + + +class TestPetPhotoView(BaseTestPetstore): + @pytest.mark.xfail( + reason="response binary format not supported", + strict=True, + ) + async def test_get_valid(self, client, data_gif): + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + "Host": "petstore.swagger.io", + } + + cookies = {"user": "1"} + response = await client.get( + "/v1/pets/1/photo", + headers=headers, + cookies=cookies, + ) + + assert await response.content.read() == data_gif + assert response.status == 200 + + async def test_post_valid(self, client, data_gif): + content_type = "image/gif" + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + "Content-Type": content_type, + "Host": "petstore.swagger.io", + } + data = { + "file": data_gif, + } + + cookies = {"user": "1"} + response = await client.post( + "/v1/pets/1/photo", + headers=headers, + data=data, + cookies=cookies, + ) + + assert not await response.text() + assert response.status == 201 diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/pets/views.py b/tests/integration/contrib/django/data/v3.0/djangoproject/pets/views.py index 8e4b38fd..16a8f1c1 100644 --- a/tests/integration/contrib/django/data/v3.0/djangoproject/pets/views.py +++ b/tests/integration/contrib/django/data/v3.0/djangoproject/pets/views.py @@ -1,3 +1,7 @@ +from base64 import b64decode + +from django.conf import settings +from django.http import FileResponse from django.http import HttpResponse from django.http import JsonResponse from rest_framework.views import APIView @@ -76,6 +80,43 @@ def get(self, request, petId): } django_response = JsonResponse(response_dict) django_response["X-Rate-Limit"] = "12" + return django_response + + @staticmethod + def get_extra_actions(): + return [] + + +class PetPhotoView(APIView): + OPENID_LOGO = b64decode( + """ +R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d +3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA +AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg +EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD +Fzk0lpcjIQA7 +""" + ) + + def get(self, request, petId): + assert request.openapi + assert not request.openapi.errors + assert request.openapi.parameters.path == { + "petId": 12, + } + django_response = FileResponse( + [self.OPENID_LOGO], + content_type="image/gif", + ) + return django_response + + def post(self, request): + assert request.openapi + assert not request.openapi.errors + + # implement file upload here + + django_response = HttpResponse(status=201) return django_response diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py b/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py index 3b4d7329..bfd93fbd 100644 --- a/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py +++ b/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py @@ -18,6 +18,7 @@ from django.urls import path from djangoproject.pets.views import PetDetailView from djangoproject.pets.views import PetListView +from djangoproject.pets.views import PetPhotoView from djangoproject.tags.views import TagListView urlpatterns = [ @@ -36,6 +37,11 @@ PetDetailView.as_view(), name="pet_detail_view", ), + path( + "v1/pets//photo", + PetPhotoView.as_view(), + name="pet_photo_view", + ), path( "v1/tags", TagListView.as_view(), diff --git a/tests/integration/contrib/django/test_django_project.py b/tests/integration/contrib/django/test_django_project.py index ed429071..0cb93529 100644 --- a/tests/integration/contrib/django/test_django_project.py +++ b/tests/integration/contrib/django/test_django_project.py @@ -395,3 +395,38 @@ def test_get_skip_response_validation(self, client): assert response.status_code == 200 assert response.content == b"success" + + +class TestPetPhotoView(BaseTestDjangoProject): + @pytest.mark.xfail( + reason="response binary format not supported", + strict=True, + ) + def test_get_valid(self, client, data_gif): + headers = { + "HTTP_AUTHORIZATION": "Basic testuser", + "HTTP_HOST": "petstore.swagger.io", + } + response = client.get("/v1/pets/12/photo", **headers) + + assert response.status_code == 200 + assert b"".join(list(response.streaming_content)) == data_gif + + @pytest.mark.xfail( + reason="request binary format not supported", + strict=True, + ) + def test_post_valid(self, client, data_gif): + client.cookies.load({"user": 1}) + content_type = "image/gif" + headers = { + "HTTP_AUTHORIZATION": "Basic testuser", + "HTTP_HOST": "petstore.swagger.io", + "HTTP_API_KEY": self.api_key_encoded, + } + response = client.post( + "/v1/pets/12/photo", data_gif, content_type, secure=True, **headers + ) + + assert response.status_code == 201 + assert not response.content diff --git a/tests/integration/contrib/falcon/data/v3.0/falconproject/__main__.py b/tests/integration/contrib/falcon/data/v3.0/falconproject/__main__.py index 6fb9d901..420601d3 100644 --- a/tests/integration/contrib/falcon/data/v3.0/falconproject/__main__.py +++ b/tests/integration/contrib/falcon/data/v3.0/falconproject/__main__.py @@ -2,11 +2,14 @@ from falconproject.openapi import openapi_middleware from falconproject.pets.resources import PetDetailResource from falconproject.pets.resources import PetListResource +from falconproject.pets.resources import PetPhotoResource app = App(middleware=[openapi_middleware]) pet_list_resource = PetListResource() pet_detail_resource = PetDetailResource() +pet_photo_resource = PetPhotoResource() app.add_route("/v1/pets", pet_list_resource) app.add_route("/v1/pets/{petId}", pet_detail_resource) +app.add_route("/v1/pets/{petId}/photo", pet_photo_resource) diff --git a/tests/integration/contrib/falcon/data/v3.0/falconproject/pets/resources.py b/tests/integration/contrib/falcon/data/v3.0/falconproject/pets/resources.py index ff22b599..be69008e 100644 --- a/tests/integration/contrib/falcon/data/v3.0/falconproject/pets/resources.py +++ b/tests/integration/contrib/falcon/data/v3.0/falconproject/pets/resources.py @@ -1,5 +1,7 @@ +from base64 import b64decode from json import dumps +from falcon.constants import MEDIA_JPEG from falcon.constants import MEDIA_JSON from falcon.status_codes import HTTP_200 from falcon.status_codes import HTTP_201 @@ -74,3 +76,23 @@ def on_get(self, request, response, petId=None): response.content_type = MEDIA_JSON response.text = dumps({"data": data}) response.set_header("X-Rate-Limit", "12") + + +class PetPhotoResource: + OPENID_LOGO = b64decode( + """ +R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d +3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA +AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg +EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD +Fzk0lpcjIQA7 +""" + ) + + def on_get(self, request, response, petId=None): + response.content_type = MEDIA_JPEG + response.stream = [self.OPENID_LOGO] + + def on_post(self, request, response, petId=None): + data = request.stream.read() + response.status = HTTP_201 diff --git a/tests/integration/contrib/falcon/test_falcon_project.py b/tests/integration/contrib/falcon/test_falcon_project.py index b9bd2a91..4afeb50b 100644 --- a/tests/integration/contrib/falcon/test_falcon_project.py +++ b/tests/integration/contrib/falcon/test_falcon_project.py @@ -365,3 +365,51 @@ def test_delete_method_invalid(self, client): } assert response.status_code == 405 assert response.json == expected_data + + +class TestPetPhotoResource(BaseTestFalconProject): + @pytest.mark.xfail( + reason="response binary format not supported", + strict=True, + ) + def test_get_valid(self, client, data_gif): + cookies = {"user": 1} + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + } + + response = client.simulate_get( + "/v1/pets/1/photo", + host="petstore.swagger.io", + headers=headers, + cookies=cookies, + ) + + assert response.content == data_gif + assert response.status_code == 200 + + @pytest.mark.xfail( + reason="request binary format not supported", + strict=True, + ) + def test_post_valid(self, client, data_json): + cookies = {"user": 1} + content_type = "image/gif" + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + "Content-Type": content_type, + } + body = dumps(data_json) + + response = client.simulate_post( + "/v1/pets/1/photo", + host="petstore.swagger.io", + headers=headers, + body=body, + cookies=cookies, + ) + + assert not response.content + assert response.status_code == 201 diff --git a/tests/integration/contrib/flask/data/v3.0/flaskproject/__init__.py b/tests/integration/contrib/flask/data/v3.0/flaskproject/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/flask/data/v3.0/flaskproject/__main__.py b/tests/integration/contrib/flask/data/v3.0/flaskproject/__main__.py new file mode 100644 index 00000000..530264fc --- /dev/null +++ b/tests/integration/contrib/flask/data/v3.0/flaskproject/__main__.py @@ -0,0 +1,11 @@ +from flask import Flask +from flaskproject.openapi import spec +from flaskproject.pets.views import PetPhotoView + +app = Flask(__name__) + +app.add_url_rule( + "/v1/pets//photo", + view_func=PetPhotoView.as_view("pet_photo", spec), + methods=["GET", "POST"], +) diff --git a/tests/integration/contrib/flask/data/v3.0/flaskproject/openapi.py b/tests/integration/contrib/flask/data/v3.0/flaskproject/openapi.py new file mode 100644 index 00000000..0f787e09 --- /dev/null +++ b/tests/integration/contrib/flask/data/v3.0/flaskproject/openapi.py @@ -0,0 +1,10 @@ +from pathlib import Path + +import yaml + +from openapi_core import Spec +from openapi_core.contrib.falcon.middlewares import FalconOpenAPIMiddleware + +openapi_spec_path = Path("tests/integration/data/v3.0/petstore.yaml") +spec_dict = yaml.load(openapi_spec_path.read_text(), yaml.Loader) +spec = Spec.from_dict(spec_dict) diff --git a/tests/integration/contrib/flask/data/v3.0/flaskproject/pets/__init__.py b/tests/integration/contrib/flask/data/v3.0/flaskproject/pets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/flask/data/v3.0/flaskproject/pets/views.py b/tests/integration/contrib/flask/data/v3.0/flaskproject/pets/views.py new file mode 100644 index 00000000..2cc15b7b --- /dev/null +++ b/tests/integration/contrib/flask/data/v3.0/flaskproject/pets/views.py @@ -0,0 +1,26 @@ +from base64 import b64decode +from io import BytesIO + +from flask.helpers import send_file + +from openapi_core.contrib.flask.views import FlaskOpenAPIView + + +class PetPhotoView(FlaskOpenAPIView): + OPENID_LOGO = b64decode( + """ +R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d +3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA +AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg +EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD +Fzk0lpcjIQA7 +""" + ) + + def get(self, petId): + fp = BytesIO(self.OPENID_LOGO) + return send_file(fp, mimetype="image/gif") + + def post(self, petId): + data = request.stream.read() + response.status = HTTP_201 diff --git a/tests/integration/contrib/flask/test_flask_project.py b/tests/integration/contrib/flask/test_flask_project.py new file mode 100644 index 00000000..b90b06ae --- /dev/null +++ b/tests/integration/contrib/flask/test_flask_project.py @@ -0,0 +1,86 @@ +import os +import sys +from base64 import b64encode + +import pytest + + +@pytest.fixture(autouse=True, scope="module") +def flask_setup(): + directory = os.path.abspath(os.path.dirname(__file__)) + flask_project_dir = os.path.join(directory, "data/v3.0") + sys.path.insert(0, flask_project_dir) + yield + sys.path.remove(flask_project_dir) + + +@pytest.fixture +def app(): + from flaskproject.__main__ import app + + app.config["SERVER_NAME"] = "petstore.swagger.io" + app.config["DEBUG"] = True + app.config["TESTING"] = True + + return app + + +@pytest.fixture +def client(app): + return app.test_client() + + +class BaseTestFlaskProject: + api_key = "12345" + + @property + def api_key_encoded(self): + api_key_bytes = self.api_key.encode("utf8") + api_key_bytes_enc = b64encode(api_key_bytes) + return str(api_key_bytes_enc, "utf8") + + +class TestPetPhotoView(BaseTestFlaskProject): + @pytest.mark.xfail( + reason="response binary format not supported", + strict=True, + ) + def test_get_valid(self, client, data_gif): + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + } + + client.set_cookie("petstore.swagger.io", "user", "1") + response = client.get( + "/v1/pets/1/photo", + headers=headers, + ) + + assert response.get_data() == data_gif + assert response.status_code == 200 + + @pytest.mark.xfail( + reason="request binary format not supported", + strict=True, + ) + def test_post_valid(self, client, data_gif): + content_type = "image/gif" + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + "Content-Type": content_type, + } + data = { + "file": data_gif, + } + + client.set_cookie("petstore.swagger.io", "user", "1") + response = client.post( + "/v1/pets/1/photo", + headers=headers, + data=data, + ) + + assert not response.text + assert response.status_code == 201 diff --git a/tests/integration/contrib/requests/data/v3.0/requests_factory.yaml b/tests/integration/contrib/requests/data/v3.1/requests_factory.yaml similarity index 100% rename from tests/integration/contrib/requests/data/v3.0/requests_factory.yaml rename to tests/integration/contrib/requests/data/v3.1/requests_factory.yaml diff --git a/tests/integration/contrib/requests/test_requests_validation.py b/tests/integration/contrib/requests/test_requests_validation.py index 2e8aee8c..df2182b0 100644 --- a/tests/integration/contrib/requests/test_requests_validation.py +++ b/tests/integration/contrib/requests/test_requests_validation.py @@ -1,7 +1,11 @@ +from base64 import b64encode + import pytest import requests import responses +from openapi_core import V30RequestUnmarshaller +from openapi_core import V30ResponseUnmarshaller from openapi_core import V31RequestUnmarshaller from openapi_core import V31ResponseUnmarshaller from openapi_core import V31WebhookRequestUnmarshaller @@ -11,10 +15,10 @@ from openapi_core.contrib.requests import RequestsOpenAPIWebhookRequest -class TestRequestsOpenAPIValidation: +class TestV31RequestsFactory: @pytest.fixture def spec(self, factory): - specfile = "contrib/requests/data/v3.0/requests_factory.yaml" + specfile = "contrib/requests/data/v3.1/requests_factory.yaml" return factory.spec_from_file(specfile) @pytest.fixture @@ -135,3 +139,84 @@ def test_webhook_response_validator_path( openapi_webhook_request, openapi_response ) assert not result.errors + + +class BaseTestPetstore: + api_key = "12345" + + @property + def api_key_encoded(self): + api_key_bytes = self.api_key.encode("utf8") + api_key_bytes_enc = b64encode(api_key_bytes) + return str(api_key_bytes_enc, "utf8") + + +class TestPetstore(BaseTestPetstore): + @pytest.fixture + def spec(self, factory): + specfile = "data/v3.0/petstore.yaml" + return factory.spec_from_file(specfile) + + @pytest.fixture + def request_unmarshaller(self, spec): + return V30RequestUnmarshaller(spec) + + @pytest.fixture + def response_unmarshaller(self, spec): + return V30ResponseUnmarshaller(spec) + + @pytest.mark.xfail( + reason="response binary format not supported", + strict=True, + ) + @responses.activate + def test_response_binary_valid(self, response_unmarshaller, data_gif): + responses.add( + responses.GET, + "http://petstore.swagger.io/v1/pets/1/photo", + body=data_gif, + content_type="image/gif", + status=200, + ) + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + } + request = requests.Request( + "GET", + "http://petstore.swagger.io/v1/pets/1/photo", + headers=headers, + ) + request_prepared = request.prepare() + session = requests.Session() + response = session.send(request_prepared) + openapi_request = RequestsOpenAPIRequest(request) + openapi_response = RequestsOpenAPIResponse(response) + result = response_unmarshaller.unmarshal( + openapi_request, openapi_response + ) + assert not result.errors + assert result.data == data_gif + + @pytest.mark.xfail( + reason="request binary format not supported", + strict=True, + ) + @responses.activate + def test_request_binary_valid(self, request_unmarshaller, data_gif): + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + "Content-Type": "image/gif", + } + request = requests.Request( + "POST", + "http://petstore.swagger.io/v1/pets/1/photo", + headers=headers, + data=data_gif, + ) + request_prepared = request.prepare() + openapi_request = RequestsOpenAPIRequest(request) + result = request_unmarshaller.unmarshal(openapi_request) + assert not result.errors + assert result.body == data_gif diff --git a/tests/integration/contrib/starlette/data/v3.0/starletteproject/__init__.py b/tests/integration/contrib/starlette/data/v3.0/starletteproject/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/starlette/data/v3.0/starletteproject/__main__.py b/tests/integration/contrib/starlette/data/v3.0/starletteproject/__main__.py new file mode 100644 index 00000000..bf1b0e7a --- /dev/null +++ b/tests/integration/contrib/starlette/data/v3.0/starletteproject/__main__.py @@ -0,0 +1,14 @@ +from starlette.applications import Starlette +from starlette.routing import Route +from starletteproject.pets.endpoints import pet_photo_endpoint + +routes = [ + Route( + "/v1/pets/{petId}/photo", pet_photo_endpoint, methods=["GET", "POST"] + ), +] + +app = Starlette( + debug=True, + routes=routes, +) diff --git a/tests/integration/contrib/starlette/data/v3.0/starletteproject/openapi.py b/tests/integration/contrib/starlette/data/v3.0/starletteproject/openapi.py new file mode 100644 index 00000000..74119263 --- /dev/null +++ b/tests/integration/contrib/starlette/data/v3.0/starletteproject/openapi.py @@ -0,0 +1,9 @@ +from pathlib import Path + +import yaml + +from openapi_core import Spec + +openapi_spec_path = Path("tests/integration/data/v3.0/petstore.yaml") +spec_dict = yaml.load(openapi_spec_path.read_text(), yaml.Loader) +spec = Spec.from_dict(spec_dict) diff --git a/tests/integration/contrib/starlette/data/v3.0/starletteproject/pets/__init__.py b/tests/integration/contrib/starlette/data/v3.0/starletteproject/pets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/starlette/data/v3.0/starletteproject/pets/endpoints.py b/tests/integration/contrib/starlette/data/v3.0/starletteproject/pets/endpoints.py new file mode 100644 index 00000000..535da4e5 --- /dev/null +++ b/tests/integration/contrib/starlette/data/v3.0/starletteproject/pets/endpoints.py @@ -0,0 +1,37 @@ +from base64 import b64decode + +from starlette.responses import Response +from starlette.responses import StreamingResponse +from starletteproject.openapi import spec + +from openapi_core import unmarshal_request +from openapi_core import unmarshal_response +from openapi_core.contrib.starlette import StarletteOpenAPIRequest +from openapi_core.contrib.starlette import StarletteOpenAPIResponse + +OPENID_LOGO = b64decode( + """ +R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d +3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA +AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg +EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD +Fzk0lpcjIQA7 +""" +) + + +def pet_photo_endpoint(request): + openapi_request = StarletteOpenAPIRequest(request) + request_unmarshalled = unmarshal_request(openapi_request, spec=spec) + if request.method == "GET": + response = StreamingResponse([OPENID_LOGO], media_type="image/gif") + elif request.method == "POST": + with request.form() as form: + filename = form["file"].filename + contents = form["file"].read() + response = Response(status_code=201) + openapi_response = StarletteOpenAPIResponse(response) + response_unmarshalled = unmarshal_response( + openapi_request, openapi_response, spec=spec + ) + return response diff --git a/tests/integration/contrib/starlette/test_starlette_project.py b/tests/integration/contrib/starlette/test_starlette_project.py new file mode 100644 index 00000000..ba406b53 --- /dev/null +++ b/tests/integration/contrib/starlette/test_starlette_project.py @@ -0,0 +1,85 @@ +import os +import sys +from base64 import b64encode + +import pytest +from starlette.testclient import TestClient + + +@pytest.fixture(autouse=True, scope="module") +def project_setup(): + directory = os.path.abspath(os.path.dirname(__file__)) + project_dir = os.path.join(directory, "data/v3.0") + sys.path.insert(0, project_dir) + yield + sys.path.remove(project_dir) + + +@pytest.fixture +def app(): + from starletteproject.__main__ import app + + return app + + +@pytest.fixture +def client(app): + return TestClient(app, base_url="http://petstore.swagger.io") + + +class BaseTestPetstore: + api_key = "12345" + + @property + def api_key_encoded(self): + api_key_bytes = self.api_key.encode("utf8") + api_key_bytes_enc = b64encode(api_key_bytes) + return str(api_key_bytes_enc, "utf8") + + +class TestPetPhotoView(BaseTestPetstore): + @pytest.mark.xfail( + reason="response binary format not supported", + strict=True, + ) + def test_get_valid(self, client, data_gif): + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + } + + cookies = {"user": "1"} + response = client.get( + "/v1/pets/1/photo", + headers=headers, + cookies=cookies, + ) + + assert response.get_data() == data_gif + assert response.status_code == 200 + + @pytest.mark.xfail( + reason="request binary format not supported", + strict=True, + ) + def test_post_valid(self, client, data_gif): + content_type = "image/gif" + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + "Content-Type": content_type, + } + data = { + "file": data_gif, + } + + cookies = {"user": "1"} + response = client.post( + "/v1/pets/1/photo", + headers=headers, + data=data, + cookies=cookies, + ) + + assert not response.text + assert response.status_code == 201 diff --git a/tests/integration/contrib/starlette/test_starlette_validation.py b/tests/integration/contrib/starlette/test_starlette_validation.py index fe147dfc..992f4821 100644 --- a/tests/integration/contrib/starlette/test_starlette_validation.py +++ b/tests/integration/contrib/starlette/test_starlette_validation.py @@ -14,7 +14,7 @@ from openapi_core.contrib.starlette import StarletteOpenAPIResponse -class TestStarletteOpenAPIValidation: +class TestV30StarletteFactory: @pytest.fixture def spec(self, factory): specfile = "contrib/starlette/data/v3.0/starlette_factory.yaml" diff --git a/tests/integration/contrib/werkzeug/test_werkzeug_validation.py b/tests/integration/contrib/werkzeug/test_werkzeug_validation.py index a940a500..f2b36ec6 100644 --- a/tests/integration/contrib/werkzeug/test_werkzeug_validation.py +++ b/tests/integration/contrib/werkzeug/test_werkzeug_validation.py @@ -15,7 +15,7 @@ class TestWerkzeugOpenAPIValidation: @pytest.fixture def spec(self, factory): - specfile = "contrib/requests/data/v3.0/requests_factory.yaml" + specfile = "contrib/requests/data/v3.1/requests_factory.yaml" return factory.spec_from_file(specfile) @pytest.fixture diff --git a/tests/integration/data/v3.0/petstore.yaml b/tests/integration/data/v3.0/petstore.yaml index 9abcd791..282b880d 100644 --- a/tests/integration/data/v3.0/petstore.yaml +++ b/tests/integration/data/v3.0/petstore.yaml @@ -173,6 +173,56 @@ paths: format: binary default: $ref: "#/components/responses/ErrorResponse" + /pets/{petId}/photo: + get: + summary: Photo for a specific pet + operationId: showPetPhotoById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: integer + format: int64 + responses: + '200': + description: Expected response to a valid request + content: + image/*: + schema: + type: string + format: binary + default: + $ref: "#/components/responses/ErrorResponse" + post: + summary: Create a pet photo + description: Creates new pet photo entry + operationId: createPetPhotoById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: integer + format: int64 + requestBody: + required: true + content: + image/*: + schema: + type: string + format: binary + responses: + '201': + description: Null response + default: + $ref: "#/components/responses/ErrorResponse" /tags: get: summary: List all tags diff --git a/tests/integration/schema/test_spec.py b/tests/integration/schema/test_spec.py index a0d447c5..5432b358 100644 --- a/tests/integration/schema/test_spec.py +++ b/tests/integration/schema/test_spec.py @@ -282,13 +282,13 @@ def test_spec(self, spec, spec_dict): if "$ref" in schema_spec: continue - schema = content.get("schema") + schema = media_type.get("schema") assert bool(schema_spec) == bool(schema) - assert schema.type.value == schema_spec["type"] - assert schema.format == schema_spec.get("format") - assert schema.required == schema_spec.get( - "required", False + assert schema["type"] == schema_spec["type"] + assert schema.getkey("format") == schema_spec.get("format") + assert schema.getkey("required") == schema_spec.get( + "required" ) components = spec.get("components")