-
Notifications
You must be signed in to change notification settings - Fork 17
fix: async middleware is equivalent to sync middleware - DIA-61984 #107
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
Changes from all commits
41c71a3
891627f
7bec281
4bee14f
c8b2434
483dc8c
2bd71b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,14 +68,6 @@ def environ(db_url, sqla_version_tuple, async_sqlalchemy_url): | |
| yield values | ||
|
|
||
|
|
||
| @fixture(scope="session") | ||
| def engine(environ): | ||
| from sqlalchemy import engine_from_config | ||
|
|
||
| engine = engine_from_config(environ, prefix="sqlalchemy_") | ||
| return engine | ||
|
|
||
|
|
||
| @fixture(autouse=True) | ||
| def tear_down(environ): | ||
| from sqlalchemy.orm.session import close_all_sessions | ||
|
|
@@ -88,6 +80,7 @@ def tear_down(environ): | |
| # reload fastapi_sqla to clear sqla deferred reflection mapping stored in Base | ||
| importlib.reload(fastapi_sqla.models) | ||
| importlib.reload(fastapi_sqla.sqla) | ||
| importlib.reload(fastapi_sqla.async_sqla) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Forgotten in previous refactor |
||
| importlib.reload(fastapi_sqla) | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| from unittest.mock import Mock | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The middleware tests were refactored to improve coverage for the async version by adopting a similar pattern as the pagination tests. |
||
|
|
||
| import httpx | ||
| from asgi_lifespan import LifespanManager | ||
| from fastapi import Depends, FastAPI, HTTPException | ||
| from pydantic import BaseModel | ||
| from pytest import fixture | ||
| from sqlalchemy import text | ||
|
|
||
|
|
||
| @fixture(scope="module", autouse=True) | ||
| def setup_tear_down(sqla_connection): | ||
| with sqla_connection.begin(): | ||
| sqla_connection.execute( | ||
| text( | ||
| """ | ||
| CREATE TABLE IF NOT EXISTS public.user ( | ||
| id integer primary key, | ||
| first_name varchar, | ||
| last_name varchar | ||
| ) | ||
| """ | ||
| ) | ||
| ) | ||
| yield | ||
| with sqla_connection.begin(): | ||
| sqla_connection.execute(text("DROP TABLE public.user")) | ||
|
|
||
|
|
||
| @fixture | ||
| def User(): | ||
| from fastapi_sqla import Base | ||
|
|
||
| class User(Base): | ||
| __tablename__ = "user" | ||
|
|
||
| return User | ||
|
|
||
|
|
||
| @fixture | ||
| def app(User): | ||
| from fastapi_sqla import Session, setup | ||
|
|
||
| app = FastAPI() | ||
| setup(app) | ||
|
|
||
| class UserIn(BaseModel): | ||
| id: int | ||
| first_name: str | ||
| last_name: str | ||
|
|
||
| @app.post("/users") | ||
| def create_user(user: UserIn, session: Session = Depends()): | ||
| session.add(User(**dict(user))) | ||
|
|
||
| @app.get("/404") | ||
| def get_users(session: Session = Depends(Session)): | ||
| raise HTTPException(status_code=404, detail="YOLO") | ||
|
|
||
| return app | ||
|
|
||
|
|
||
| @fixture | ||
| def mock_middleware(app: FastAPI): | ||
| mock_middleware = Mock() | ||
|
|
||
| @app.middleware("http") | ||
| async def a_middleware(request, call_next): | ||
| res = await call_next(request) | ||
| mock_middleware() | ||
| return res | ||
|
|
||
| return mock_middleware | ||
|
|
||
|
|
||
| @fixture | ||
| async def client(app, mock_middleware): | ||
| async with LifespanManager(app): | ||
| transport = httpx.ASGITransport(app=app, raise_app_exceptions=False) | ||
| async with httpx.AsyncClient( | ||
| transport=transport, base_url="http://example.local" | ||
| ) as client: | ||
| yield client | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| from unittest.mock import patch | ||
|
|
||
| from fastapi import Depends, FastAPI, HTTPException | ||
| from pydantic import BaseModel | ||
| from pytest import fixture, mark | ||
| from sqlalchemy import text | ||
| from structlog.testing import capture_logs | ||
|
|
||
| pytestmark = [mark.sqlalchemy("1.4"), mark.require_asyncpg] | ||
|
|
||
|
|
||
| @fixture | ||
| def app(User): | ||
| from fastapi_sqla import AsyncSession, setup | ||
|
|
||
| app = FastAPI() | ||
| setup(app) | ||
|
|
||
| class UserIn(BaseModel): | ||
| id: int | ||
| first_name: str | ||
| last_name: str | ||
|
|
||
| @app.post("/users") | ||
| def create_user(user: UserIn, session: AsyncSession = Depends()): | ||
| session.add(User(**dict(user))) | ||
|
|
||
| @app.get("/404") | ||
| def get_users(session: AsyncSession = Depends(AsyncSession)): | ||
| raise HTTPException(status_code=404, detail="YOLO") | ||
|
|
||
| return app | ||
|
|
||
|
|
||
| async def test_async_session_dependency(client, faker, async_session): | ||
| userid = faker.unique.random_int() | ||
| first_name = faker.first_name() | ||
| last_name = faker.last_name() | ||
| res = await client.post( | ||
| "/users", json={"id": userid, "first_name": first_name, "last_name": last_name} | ||
| ) | ||
| assert res.status_code == 200, res.json() | ||
| row = ( | ||
| await async_session.execute( | ||
| text(f"select * from public.user where id = {userid}") | ||
| ) | ||
| ).fetchone() | ||
| assert row == (userid, first_name, last_name) | ||
|
|
||
|
|
||
| @fixture | ||
| async def user_1(async_sqla_connection): | ||
| async with async_sqla_connection.begin(): | ||
| await async_sqla_connection.execute( | ||
| text("INSERT INTO public.user VALUES (1, 'bob', 'morane') ") | ||
| ) | ||
| yield | ||
| async with async_sqla_connection.begin(): | ||
| await async_sqla_connection.execute( | ||
| text("DELETE FROM public.user WHERE id = 1") | ||
| ) | ||
|
|
||
|
|
||
| async def test_commit_error_returns_500(client, user_1, mock_middleware): | ||
| with capture_logs() as caplog: | ||
| res = await client.post( | ||
| "/users", | ||
| json={"id": 1, "first_name": "Bob", "last_name": "Morane"}, | ||
| headers={"origin": "localhost"}, | ||
| ) | ||
|
|
||
| assert res.status_code == 500 | ||
|
|
||
| assert { | ||
| "event": "commit failed, returning http error", | ||
| "exc_info": True, | ||
| "log_level": "error", | ||
| } in caplog | ||
|
|
||
| assert { | ||
| "event": "http error, rolling back possibly uncommitted changes", | ||
| "log_level": "warning", | ||
| "status_code": 500, | ||
| } in caplog | ||
|
|
||
| mock_middleware.assert_called_once() | ||
|
|
||
|
|
||
| async def test_rollback_on_http_exception(client, mock_middleware): | ||
| with patch("fastapi_sqla.async_sqla.open_session") as open_session: | ||
| session = open_session.return_value.__aenter__.return_value | ||
|
|
||
| await client.get("/404") | ||
|
|
||
| session.rollback.assert_awaited_once_with() | ||
| mock_middleware.assert_called_once() | ||
|
|
||
|
|
||
| async def test_rollback_on_http_exception_silent(client, mock_middleware): | ||
| with capture_logs() as caplog: | ||
| await client.get("/404") | ||
|
|
||
| mock_middleware.assert_called_once() | ||
|
|
||
| assert { | ||
| "event": "http error, rolling back possibly uncommitted changes", | ||
| "log_level": "warning", | ||
| "status_code": 404, | ||
| } not in caplog |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We weren't actually testing it, and it was flagged as not covered after I cleaned up
test_startup. I don't think it should be that PR responsibility to add coverage for this case in this fixtureThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 weird that it was flagged as not covered as that marker is used in tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Anyway, agree it is not the responsibility of current PR to cover this)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was previously covered in
test_startup, but since we were setting themark.usefixtures("patch_engine_from_config", "patch_new_engine")at the top of the file, but then for every test we were using@mark.dont_patch_engines, we weren't actually testing anything in terms of what the expected behavior is.