From 64f4dd0d22d59aa526eae9dbc7dfc1341be8550b Mon Sep 17 00:00:00 2001 From: p1c2u Date: Thu, 30 Nov 2023 11:04:16 +0000 Subject: [PATCH] FastAPI integration --- docs/integrations/fastapi.rst | 58 +++ docs/integrations/index.rst | 1 + openapi_core/contrib/fastapi/__init__.py | 9 + openapi_core/contrib/fastapi/middlewares.py | 5 + openapi_core/contrib/fastapi/requests.py | 8 + openapi_core/contrib/fastapi/responses.py | 10 + poetry.lock | 46 ++- pyproject.toml | 3 + .../data/v3.0/fastapiproject/__init__.py | 0 .../data/v3.0/fastapiproject/__main__.py | 9 + .../data/v3.0/fastapiproject/openapi.py | 9 + .../v3.0/fastapiproject/routers/__init__.py | 0 .../data/v3.0/fastapiproject/routers/pets.py | 109 +++++ .../contrib/fastapi/test_fastapi_project.py | 383 ++++++++++++++++++ 14 files changed, 637 insertions(+), 13 deletions(-) create mode 100644 docs/integrations/fastapi.rst create mode 100644 openapi_core/contrib/fastapi/__init__.py create mode 100644 openapi_core/contrib/fastapi/middlewares.py create mode 100644 openapi_core/contrib/fastapi/requests.py create mode 100644 openapi_core/contrib/fastapi/responses.py create mode 100644 tests/integration/contrib/fastapi/data/v3.0/fastapiproject/__init__.py create mode 100644 tests/integration/contrib/fastapi/data/v3.0/fastapiproject/__main__.py create mode 100644 tests/integration/contrib/fastapi/data/v3.0/fastapiproject/openapi.py create mode 100644 tests/integration/contrib/fastapi/data/v3.0/fastapiproject/routers/__init__.py create mode 100644 tests/integration/contrib/fastapi/data/v3.0/fastapiproject/routers/pets.py create mode 100644 tests/integration/contrib/fastapi/test_fastapi_project.py diff --git a/docs/integrations/fastapi.rst b/docs/integrations/fastapi.rst new file mode 100644 index 00000000..bd3abde1 --- /dev/null +++ b/docs/integrations/fastapi.rst @@ -0,0 +1,58 @@ +FastAPI +========= + +This section describes integration with `FastAPI `__ ASGI framework. + +.. note:: + + FastAPI also provides OpenAPI support. The main difference is that, unlike FastAPI's code-first approach, OpenAPI-core allows you to laverage your existing specification that alligns with API-First approach. You can read more about API-first vs. code-first in the [Guide to API-first](https://www.postman.com/api-first/). + +Middleware +---------- + +FastAPI can be integrated by `middleware `__ to apply OpenAPI validation to your entire application. + +Add ``FastAPIOpenAPIMiddleware`` with OpenAPI object to your ``middleware`` list. + +.. code-block:: python + :emphasize-lines: 2,5 + + from fastapi import FastAPI + from openapi_core.contrib.fastapi.middlewares import FastAPIOpenAPIMiddleware + + app = FastAPI() + app.add_middleware(FastAPIOpenAPIMiddleware, openapi=openapi) + +After that all your requests and responses will be validated. + +Also you have access to unmarshal result object with all unmarshalled request data through ``openapi`` scope of request object. + +.. code-block:: python + + async def homepage(request): + # get parameters object with path, query, cookies and headers parameters + unmarshalled_params = request.scope["openapi"].parameters + # or specific location parameters + unmarshalled_path_params = request.scope["openapi"].parameters.path + + # get body + unmarshalled_body = request.scope["openapi"].body + + # get security data + unmarshalled_security = request.scope["openapi"].security + +Response validation +^^^^^^^^^^^^^^^^^^^ + +You can skip response validation process: by setting ``response_cls`` to ``None`` + +.. code-block:: python + :emphasize-lines: 2 + + app = FastAPI() + app.add_middleware(FastAPIOpenAPIMiddleware, openapi=openapi, response_cls=None) + +Low level +--------- + +For low level integration see `Starlette `_ integration. diff --git a/docs/integrations/index.rst b/docs/integrations/index.rst index cd046758..f48c8cc9 100644 --- a/docs/integrations/index.rst +++ b/docs/integrations/index.rst @@ -10,6 +10,7 @@ Openapi-core integrates with your popular libraries and frameworks. Each integra bottle django falcon + fastapi flask pyramid requests diff --git a/openapi_core/contrib/fastapi/__init__.py b/openapi_core/contrib/fastapi/__init__.py new file mode 100644 index 00000000..d658ddcf --- /dev/null +++ b/openapi_core/contrib/fastapi/__init__.py @@ -0,0 +1,9 @@ +from openapi_core.contrib.fastapi.middlewares import FastAPIOpenAPIMiddleware +from openapi_core.contrib.fastapi.requests import FastAPIOpenAPIRequest +from openapi_core.contrib.fastapi.responses import FastAPIOpenAPIResponse + +__all__ = [ + "FastAPIOpenAPIMiddleware", + "FastAPIOpenAPIRequest", + "FastAPIOpenAPIResponse", +] diff --git a/openapi_core/contrib/fastapi/middlewares.py b/openapi_core/contrib/fastapi/middlewares.py new file mode 100644 index 00000000..5aedf224 --- /dev/null +++ b/openapi_core/contrib/fastapi/middlewares.py @@ -0,0 +1,5 @@ +from openapi_core.contrib.starlette.middlewares import ( + StarletteOpenAPIMiddleware as FastAPIOpenAPIMiddleware, +) + +__all__ = ["FastAPIOpenAPIMiddleware"] diff --git a/openapi_core/contrib/fastapi/requests.py b/openapi_core/contrib/fastapi/requests.py new file mode 100644 index 00000000..c70d8c81 --- /dev/null +++ b/openapi_core/contrib/fastapi/requests.py @@ -0,0 +1,8 @@ +from fastapi import Request + +from openapi_core.contrib.starlette.requests import StarletteOpenAPIRequest + + +class FastAPIOpenAPIRequest(StarletteOpenAPIRequest): + def __init__(self, request: Request): + super().__init__(request) diff --git a/openapi_core/contrib/fastapi/responses.py b/openapi_core/contrib/fastapi/responses.py new file mode 100644 index 00000000..6ef7ea22 --- /dev/null +++ b/openapi_core/contrib/fastapi/responses.py @@ -0,0 +1,10 @@ +from typing import Optional + +from fastapi import Response + +from openapi_core.contrib.starlette.responses import StarletteOpenAPIResponse + + +class FastAPIOpenAPIResponse(StarletteOpenAPIResponse): + def __init__(self, response: Response, data: Optional[bytes] = None): + super().__init__(response, data=data) diff --git a/poetry.lock b/poetry.lock index 4e24afa4..02d16970 100644 --- a/poetry.lock +++ b/poetry.lock @@ -151,24 +151,24 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} [[package]] name = "anyio" -version = "4.0.0" +version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, - {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.22)"] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] [[package]] name = "appdirs" @@ -711,6 +711,25 @@ files = [ {file = "falcon-3.1.3.tar.gz", hash = "sha256:23335dbccd44f29e85ec55f2f35d5a0bc12bd7a509f641ab81f5c64b65626263"}, ] +[[package]] +name = "fastapi" +version = "0.108.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.108.0-py3-none-any.whl", hash = "sha256:8c7bc6d315da963ee4cdb605557827071a9a7f95aeb8fcdd3bde48cdc8764dd7"}, + {file = "fastapi-0.108.0.tar.gz", hash = "sha256:5056e504ac6395bf68493d71fcfc5352fdbd5fda6f88c21f6420d80d81163296"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.29.0,<0.33.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + [[package]] name = "filelock" version = "3.13.1" @@ -2292,13 +2311,13 @@ test = ["pytest", "pytest-cov"] [[package]] name = "starlette" -version = "0.37.1" +version = "0.32.0.post1" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" files = [ - {file = "starlette-0.37.1-py3-none-any.whl", hash = "sha256:92a816002d4e8c552477b089520e3085bb632e854eb32cef99acb6f6f7830b69"}, - {file = "starlette-0.37.1.tar.gz", hash = "sha256:345cfd562236b557e76a045715ac66fdc355a1e7e617b087834a76a87dcc6533"}, + {file = "starlette-0.32.0.post1-py3-none-any.whl", hash = "sha256:cd0cb10ddb49313f609cedfac62c8c12e56c7314b66d89bb077ba228bada1b09"}, + {file = "starlette-0.32.0.post1.tar.gz", hash = "sha256:e54e2b7e2fb06dff9eac40133583f10dfa05913f5a85bf26f427c7a40a9a3d02"}, ] [package.dependencies] @@ -2306,7 +2325,7 @@ anyio = ">=3.4.0,<5" typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] [[package]] name = "strict-rfc3339" @@ -2526,6 +2545,7 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p aiohttp = ["aiohttp", "multidict"] django = ["django"] falcon = ["falcon"] +fastapi = ["fastapi"] flask = ["flask"] requests = ["requests"] starlette = ["aioitertools", "starlette"] @@ -2533,4 +2553,4 @@ starlette = ["aioitertools", "starlette"] [metadata] lock-version = "2.0" python-versions = "^3.8.0" -content-hash = "09d51553f494e21e03261f7c996c978580e73472731c1b64ac17378bbfe70cb4" +content-hash = "a0c24b771433b05d6e5ee543c0529ecfeb361c871f974f2129a95c99df2326cb" diff --git a/pyproject.toml b/pyproject.toml index a3b1dfa2..2d0a536c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,10 +76,12 @@ jsonschema-path = "^0.3.1" jsonschema = "^4.18.0" multidict = {version = "^6.0.4", optional = true} aioitertools = {version = "^0.11.0", optional = true} +fastapi = {version = "^0.108.0", optional = true} [tool.poetry.extras] django = ["django"] falcon = ["falcon"] +fastapi = ["fastapi"] flask = ["flask"] requests = ["requests"] aiohttp = ["aiohttp", "multidict"] @@ -108,6 +110,7 @@ aiohttp = "^3.8.4" pytest-aiohttp = "^1.0.4" bump2version = "^1.0.1" pyflakes = "^3.1.0" +fastapi = "^0.108.0" [tool.poetry.group.docs.dependencies] sphinx = ">=5.3,<8.0" diff --git a/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/__init__.py b/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/__main__.py b/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/__main__.py new file mode 100644 index 00000000..916cd2cd --- /dev/null +++ b/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/__main__.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI +from fastapiproject.openapi import openapi +from fastapiproject.routers import pets + +from openapi_core.contrib.fastapi.middlewares import FastAPIOpenAPIMiddleware + +app = FastAPI() +app.add_middleware(FastAPIOpenAPIMiddleware, openapi=openapi) +app.include_router(pets.router) diff --git a/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/openapi.py b/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/openapi.py new file mode 100644 index 00000000..4ca6d9fa --- /dev/null +++ b/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/openapi.py @@ -0,0 +1,9 @@ +from pathlib import Path + +import yaml + +from openapi_core import OpenAPI + +openapi_spec_path = Path("tests/integration/data/v3.0/petstore.yaml") +spec_dict = yaml.load(openapi_spec_path.read_text(), yaml.Loader) +openapi = OpenAPI.from_dict(spec_dict) diff --git a/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/routers/__init__.py b/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/routers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/routers/pets.py b/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/routers/pets.py new file mode 100644 index 00000000..d4b763b9 --- /dev/null +++ b/tests/integration/contrib/fastapi/data/v3.0/fastapiproject/routers/pets.py @@ -0,0 +1,109 @@ +from base64 import b64decode + +from fastapi import APIRouter +from fastapi import Body +from fastapi import Request +from fastapi import Response +from fastapi import status + +try: + from typing import Annotated +except ImportError: + from typing_extensions import Annotated + +OPENID_LOGO = b64decode( + """ +R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d +3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA +AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg +EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD +Fzk0lpcjIQA7 +""" +) + + +router = APIRouter( + prefix="/v1/pets", + tags=["pets"], + responses={404: {"description": "Not found"}}, +) + + +@router.get("") +async def list_pets(request: Request, response: Response): + assert request.scope["openapi"] + assert not request.scope["openapi"].errors + assert request.scope["openapi"].parameters.query == { + "page": 1, + "limit": 12, + "search": "", + } + data = [ + { + "id": 12, + "name": "Cat", + "ears": { + "healthy": True, + }, + }, + ] + response.headers["X-Rate-Limit"] = "12" + return {"data": data} + + +@router.post("") +async def create_pet(request: Request): + assert request.scope["openapi"].parameters.cookie == { + "user": 1, + } + assert request.scope["openapi"].parameters.header == { + "api-key": "12345", + } + assert request.scope["openapi"].body.__class__.__name__ == "PetCreate" + assert request.scope["openapi"].body.name in ["Cat", "Bird"] + if request.scope["openapi"].body.name == "Cat": + assert request.scope["openapi"].body.ears.__class__.__name__ == "Ears" + assert request.scope["openapi"].body.ears.healthy is True + if request.scope["openapi"].body.name == "Bird": + assert ( + request.scope["openapi"].body.wings.__class__.__name__ == "Wings" + ) + assert request.scope["openapi"].body.wings.healthy is True + + headers = { + "X-Rate-Limit": "12", + } + return Response(status_code=status.HTTP_201_CREATED, headers=headers) + + +@router.get("/{petId}") +async def detail_pet(request: Request, response: Response): + assert request.scope["openapi"] + assert not request.scope["openapi"].errors + assert request.scope["openapi"].parameters.path == { + "petId": 12, + } + data = { + "id": 12, + "name": "Cat", + "ears": { + "healthy": True, + }, + } + response.headers["X-Rate-Limit"] = "12" + return { + "data": data, + } + + +@router.get("/{petId}/photo") +async def download_pet_photo(): + return Response(content=OPENID_LOGO, media_type="image/gif") + + +@router.post("/{petId}/photo") +async def upload_pet_photo( + image: Annotated[bytes, Body(media_type="image/jpg")], +): + assert image == OPENID_LOGO + return Response(status_code=status.HTTP_201_CREATED) diff --git a/tests/integration/contrib/fastapi/test_fastapi_project.py b/tests/integration/contrib/fastapi/test_fastapi_project.py new file mode 100644 index 00000000..e8d795c6 --- /dev/null +++ b/tests/integration/contrib/fastapi/test_fastapi_project.py @@ -0,0 +1,383 @@ +import os +import sys +from base64 import b64encode + +import pytest +from fastapi.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 fastapiproject.__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 TestPetListEndpoint(BaseTestPetstore): + def test_get_no_required_param(self, client): + headers = { + "Authorization": "Basic testuser", + } + + with pytest.warns(DeprecationWarning): + response = client.get("/v1/pets", headers=headers) + + expected_data = { + "errors": [ + { + "type": ( + "" + ), + "status": 400, + "title": "Missing required query parameter: limit", + } + ] + } + assert response.status_code == 400 + assert response.json() == expected_data + + def test_get_valid(self, client): + data_json = { + "limit": 12, + } + headers = { + "Authorization": "Basic testuser", + } + + with pytest.warns(DeprecationWarning): + response = client.get( + "/v1/pets", + params=data_json, + headers=headers, + ) + + expected_data = { + "data": [ + { + "id": 12, + "name": "Cat", + "ears": { + "healthy": True, + }, + }, + ], + } + assert response.status_code == 200 + assert response.json() == expected_data + + def test_post_server_invalid(self, client): + response = client.post("/v1/pets") + + expected_data = { + "errors": [ + { + "type": ( + "" + ), + "status": 400, + "title": ( + "Server not found for " + "http://petstore.swagger.io/v1/pets" + ), + } + ] + } + assert response.status_code == 400 + assert response.json() == expected_data + + def test_post_required_header_param_missing(self, client): + client.cookies.set("user", "1") + pet_name = "Cat" + pet_tag = "cats" + pet_street = "Piekna" + pet_city = "Warsaw" + pet_healthy = False + data_json = { + "name": pet_name, + "tag": pet_tag, + "position": 2, + "address": { + "street": pet_street, + "city": pet_city, + }, + "healthy": pet_healthy, + "wings": { + "healthy": pet_healthy, + }, + } + content_type = "application/json" + headers = { + "Authorization": "Basic testuser", + "Content-Type": content_type, + } + response = client.post( + "https://staging.gigantic-server.com/v1/pets", + json=data_json, + headers=headers, + ) + + expected_data = { + "errors": [ + { + "type": ( + "" + ), + "status": 400, + "title": "Missing required header parameter: api-key", + } + ] + } + assert response.status_code == 400 + assert response.json() == expected_data + + def test_post_media_type_invalid(self, client): + client.cookies.set("user", "1") + content = "data" + content_type = "text/html" + headers = { + "Authorization": "Basic testuser", + "Content-Type": content_type, + "Api-Key": self.api_key_encoded, + } + response = client.post( + "https://staging.gigantic-server.com/v1/pets", + content=content, + headers=headers, + ) + + expected_data = { + "errors": [ + { + "type": ( + "" + ), + "status": 415, + "title": ( + "Content for the following mimetype not found: " + "text/html. " + "Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'text/plain']" + ), + } + ] + } + assert response.status_code == 415 + assert response.json() == expected_data + + def test_post_required_cookie_param_missing(self, client): + data_json = { + "id": 12, + "name": "Cat", + "ears": { + "healthy": True, + }, + } + content_type = "application/json" + headers = { + "Authorization": "Basic testuser", + "Content-Type": content_type, + "Api-Key": self.api_key_encoded, + } + response = client.post( + "https://staging.gigantic-server.com/v1/pets", + json=data_json, + headers=headers, + ) + + expected_data = { + "errors": [ + { + "type": ( + "" + ), + "status": 400, + "title": "Missing required cookie parameter: user", + } + ] + } + assert response.status_code == 400 + assert response.json() == expected_data + + @pytest.mark.parametrize( + "data_json", + [ + { + "id": 12, + "name": "Cat", + "ears": { + "healthy": True, + }, + }, + { + "id": 12, + "name": "Bird", + "wings": { + "healthy": True, + }, + }, + ], + ) + def test_post_valid(self, client, data_json): + client.cookies.set("user", "1") + content_type = "application/json" + headers = { + "Authorization": "Basic testuser", + "Content-Type": content_type, + "Api-Key": self.api_key_encoded, + } + response = client.post( + "https://staging.gigantic-server.com/v1/pets", + json=data_json, + headers=headers, + ) + + assert response.status_code == 201 + assert not response.content + + +class TestPetDetailEndpoint(BaseTestPetstore): + def test_get_server_invalid(self, client): + response = client.get("http://testserver/v1/pets/12") + + expected_data = { + "errors": [ + { + "type": ( + "" + ), + "status": 400, + "title": ( + "Server not found for " "http://testserver/v1/pets/12" + ), + } + ] + } + assert response.status_code == 400 + assert response.json() == expected_data + + def test_get_unauthorized(self, client): + response = client.get("/v1/pets/12") + + expected_data = { + "errors": [ + { + "type": ( + "" + ), + "status": 403, + "title": ( + "Security not found. Schemes not valid for any " + "requirement: [['petstore_auth']]" + ), + } + ] + } + assert response.status_code == 403 + assert response.json() == expected_data + + def test_delete_method_invalid(self, client): + headers = { + "Authorization": "Basic testuser", + } + response = client.delete("/v1/pets/12", headers=headers) + + expected_data = { + "errors": [ + { + "type": ( + "" + ), + "status": 405, + "title": ( + "Operation delete not found for " + "http://petstore.swagger.io/v1/pets/12" + ), + } + ] + } + assert response.status_code == 405 + assert response.json() == expected_data + + def test_get_valid(self, client): + headers = { + "Authorization": "Basic testuser", + } + response = client.get("/v1/pets/12", headers=headers) + + expected_data = { + "data": { + "id": 12, + "name": "Cat", + "ears": { + "healthy": True, + }, + }, + } + assert response.status_code == 200 + assert response.json() == expected_data + + +class TestPetPhotoEndpoint(BaseTestPetstore): + def test_get_valid(self, client, data_gif): + client.cookies.set("user", "1") + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + } + + response = client.get( + "/v1/pets/1/photo", + headers=headers, + ) + + assert response.content == data_gif + assert response.status_code == 200 + + def test_post_valid(self, client, data_gif): + client.cookies.set("user", "1") + content_type = "image/gif" + headers = { + "Authorization": "Basic testuser", + "Api-Key": self.api_key_encoded, + "Content-Type": content_type, + } + + response = client.post( + "/v1/pets/1/photo", + headers=headers, + content=data_gif, + ) + + assert not response.text + assert response.status_code == 201 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy