diff --git a/src/assertical/fixtures/fastapi.py b/src/assertical/fixtures/fastapi.py index da511ee..91436b1 100644 --- a/src/assertical/fixtures/fastapi.py +++ b/src/assertical/fixtures/fastapi.py @@ -5,7 +5,7 @@ import uvicorn from asgi_lifespan import LifespanManager from fastapi import FastAPI -from httpx import ASGITransport, AsyncClient +from httpx import ASGITransport, AsyncClient, Request, URL from httpx._types import AuthTypes @@ -41,12 +41,35 @@ async def down(self) -> None: await self._serve_task +class ContentTypeAsyncClient(AsyncClient): + """AsyncClient implementation that assigns content-type for bodied requests.""" + + def __init__(self, *, content_type: str, **kwargs: Any): + super().__init__(**kwargs) + self.content_type = content_type + + def build_request(self, method: str, url: URL | str, **kwargs: Any) -> Request: + """Overloaded implementation from parent class. + + This detects if there is a body associated with the request and sets a content-type + """ + request = super().build_request(method, url, **kwargs) + + if request.content: + if "content-type" not in request.headers: + request.headers["Content-Type"] = self.content_type + + return request + + @asynccontextmanager async def start_app_with_client( app: FastAPI, client_auth: Optional[AuthTypes] = None ) -> AsyncGenerator[AsyncClient, None]: """Creates an AsyncClient for a test app and returns it as an AsyncGenerator for use with a fixture: + The default content-type for bodied requests is application/json + Usage: async with start_app_with_client(app) as client: @@ -59,7 +82,9 @@ async def start_app_with_client( """ async with LifespanManager(app): # This ensures that startup events are fired when the app starts - async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test", auth=client_auth) as c: + async with ContentTypeAsyncClient( + content_type="application/json", transport=ASGITransport(app=app), base_url="http://test", auth=client_auth + ) as c: yield c diff --git a/tests/fixtures/test_fastapi.py b/tests/fixtures/test_fastapi.py index 3091a01..3c4f8bb 100644 --- a/tests/fixtures/test_fastapi.py +++ b/tests/fixtures/test_fastapi.py @@ -1,4 +1,5 @@ from contextlib import asynccontextmanager +import json import pytest from fastapi import FastAPI @@ -29,8 +30,25 @@ async def added_later(): async def hello_world(): return {"msg": "Hello World"} + @test_app.post("/hello_world") + async def hello_world_create(): + return None + return test_app +@pytest.mark.anyio +async def test_client_content_type_set(): + """Ensures that a request content-type header is only set for bodied requests.""" + app = generate_test_app() + + async with start_app_with_client(app) as client: + body = json.dumps({"msg": "hello world"}) + response = await client.post("/hello_world", content=body) + assert response.request.headers.get("content-type") == "application/json" + + response = await client.get("/hello_world") + assert not response.request.headers.get("content-type") + @pytest.mark.parametrize("multi_iteration", [1, 2, 3]) @pytest.mark.anyio