From 7f02108f8bb3c389c59a88439229b53b5284dd25 Mon Sep 17 00:00:00 2001 From: Baiheng Xie <874256269@qq.com> Date: Thu, 5 Mar 2026 16:07:14 +0800 Subject: [PATCH 1/5] fix: update dependencies version --- pyproject.toml | 29 +++---- tests/conftest.py | 23 ++++-- tests/integration/conftest.py | 108 +++++++++++++++++++++++++++ tests/integration/test_audit_rbac.py | 98 ------------------------ tests/integration/test_rbac.py | 106 -------------------------- tests/shared/test_mixins.py | 10 ++- 6 files changed, 145 insertions(+), 229 deletions(-) create mode 100644 tests/integration/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 2cdd8c4..a6cab5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,31 +3,31 @@ name = "fastapi-boilerplate" version = "0.1.0" requires-python = ">=3.12" dependencies = [ - "aiosqlite>=0.21.0", - "alembic>=1.17.2", + "aiosqlite>=0.22.1", + "alembic>=1.18.4", "asyncpg>=0.31.0", - "fastapi>=0.115.0", - "fastapi-pagination[sqlalchemy]>=0.15.3", - "fastapi-users[sqlalchemy]>=15.0.1", + "fastapi>=0.129.0", + "fastapi-pagination[sqlalchemy]>=0.15.10", + "fastapi-users[sqlalchemy]>=15.0.4", "httpx-oauth>=0.16.1", "loguru>=0.7.3", - "pydantic-settings>=2.12.0", - "tenacity>=9.0.0", - "redis[hiredis]>=7.1.0", - "sqlmodel>=0.0.27", - "uvicorn[standard]>=0.32.0", + "pydantic-settings>=2.13.1", + "tenacity>=9.1.4", + "redis[hiredis]>=7.2.0", + "sqlmodel>=0.0.34", + "uvicorn[standard]>=0.41.0", ] [dependency-groups] dev = [ - "fakeredis>=2.32.1", - "locust>=2.32.5", - "pytest>=8.3.3", + "fakeredis>=2.34.0", + "locust>=2.43.3", + "pytest>=9.0.2", "pytest-asyncio>=1.2.0", "pytest-cov>=6.1.1", "pytest-timeout>=2.4.0", "python-dotenv>=1.2.1", - "ruff>=0.12.8", + "ruff>=0.15.1", ] [build-system] @@ -42,6 +42,7 @@ packages = ["src"] [tool.pytest.ini_options] asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" # filterwarnings = ["error::DeprecationWarning"] timeout = 5 pythonpath = ["."] diff --git a/tests/conftest.py b/tests/conftest.py index 4b2eba9..0569e81 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ from fastapi import FastAPI from httpx import ASGITransport, AsyncClient from loguru import logger +from redis.asyncio.client import Redis as AsyncRedisClient from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker @@ -21,6 +22,11 @@ dotenv.load_dotenv(dotenv_path=env_path) +class _FakeRedisClientNoDeprecatedParams(AsyncRedisClient): + def __init__(self, *, decode_responses: bool = False, **kwargs): + super().__init__(decode_responses=decode_responses, **kwargs) + + @pytest.fixture def settings(): from src.config import get_settings @@ -99,12 +105,13 @@ async def override_get_session(): app.dependency_overrides[get_session] = override_get_session - yield async_session - - async with engine.begin() as conn: - await conn.run_sync(SQLModel.metadata.drop_all) - - app.dependency_overrides.clear() + try: + yield async_session + finally: + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.drop_all) + await engine.dispose() + app.dependency_overrides.clear() @pytest.fixture @@ -117,7 +124,9 @@ async def local_cache(): @pytest.fixture async def redis_cache(): cache = RedisCache(host="localhost", port=6379, db=1) - cache._client = FakeAsyncRedis(decode_responses=True) + cache._client = FakeAsyncRedis( + decode_responses=True, client_class=_FakeRedisClientNoDeprecatedParams + ) await cache.clear() yield cache await cache.clear() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..9bacc64 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,108 @@ +import pytest + +from src.auth.models import User +from src.auth.rbac.models import Permission, RolePermission, UserRole + + +@pytest.fixture +async def rbac_data(test_db): + async with test_db() as session: + perm_read = Permission(id=1, code="user:read", name="Read Users", module="user") + perm_write = Permission( + id=2, code="user:write", name="Write Users", module="user" + ) + perm_delete = Permission( + id=3, code="user:delete", name="Delete Users", module="user" + ) + perm_admin_all = Permission( + id=4, code="admin:*", name="Admin All", module="admin" + ) + session.add_all([perm_read, perm_write, perm_delete, perm_admin_all]) + await session.commit() + + role_perms = [ + RolePermission(role_id=1, permission_id=1), + RolePermission(role_id=1, permission_id=2), + RolePermission(role_id=1, permission_id=3), + RolePermission(role_id=1, permission_id=4), + RolePermission(role_id=2, permission_id=1), + ] + session.add_all(role_perms) + await session.commit() + + +@pytest.fixture +async def admin_user(test_db, rbac_data): + from fastapi_users.password import PasswordHelper + + password_helper = PasswordHelper() + hashed_password = password_helper.hash("admin123") + + async with test_db() as session: + user = User( + username="adminuser", + email="admin@example.com", + hashed_password=hashed_password, + is_active=True, + is_verified=True, + is_superuser=False, + ) + session.add(user) + await session.commit() + await session.refresh(user) + + user_role = UserRole(user_id=user.id, role_id=1) + session.add(user_role) + await session.commit() + + yield user + + +@pytest.fixture +async def regular_user(test_db, rbac_data): + from fastapi_users.password import PasswordHelper + + password_helper = PasswordHelper() + hashed_password = password_helper.hash("user123") + + async with test_db() as session: + user = User( + username="regularuser", + email="user@example.com", + hashed_password=hashed_password, + is_active=True, + is_verified=True, + is_superuser=False, + ) + session.add(user) + await session.commit() + await session.refresh(user) + + user_role = UserRole(user_id=user.id, role_id=2) + session.add(user_role) + await session.commit() + + yield user + + +@pytest.fixture +async def superuser_user(test_db, rbac_data): + from fastapi_users.password import PasswordHelper + + password_helper = PasswordHelper() + hashed_password = password_helper.hash("super123") + + async with test_db() as session: + user = User( + username="superuser", + email="super@example.com", + hashed_password=hashed_password, + is_active=True, + is_verified=True, + is_superuser=True, + ) + session.add(user) + await session.commit() + await session.refresh(user) + + yield user diff --git a/tests/integration/test_audit_rbac.py b/tests/integration/test_audit_rbac.py index fec2878..0a8669a 100644 --- a/tests/integration/test_audit_rbac.py +++ b/tests/integration/test_audit_rbac.py @@ -4,108 +4,10 @@ from sqlalchemy import select from src.auth import require_permissions, require_roles -from src.auth.models import User -from src.auth.rbac.models import Permission, RolePermission, UserRole from src.audit.schemas import AuditAction, AuditLog, AuditResult from src.shared.errors import ErrorCode -@pytest.fixture -async def rbac_data(test_db): - async with test_db() as session: - perm_read = Permission(id=1, code="user:read", name="Read Users", module="user") - perm_write = Permission( - id=2, code="user:write", name="Write Users", module="user" - ) - session.add_all([perm_read, perm_write]) - await session.commit() - - role_perms = [ - RolePermission(role_id=1, permission_id=1), - RolePermission(role_id=1, permission_id=2), - RolePermission(role_id=2, permission_id=1), - ] - session.add_all(role_perms) - await session.commit() - - -@pytest.fixture -async def admin_user(test_db, rbac_data): - from fastapi_users.password import PasswordHelper - - password_helper = PasswordHelper() - hashed_password = password_helper.hash("admin123") - - async with test_db() as session: - user = User( - username="adminuser", - email="admin@example.com", - hashed_password=hashed_password, - is_active=True, - is_verified=True, - is_superuser=False, - ) - session.add(user) - await session.commit() - await session.refresh(user) - - user_role = UserRole(user_id=user.id, role_id=1) - session.add(user_role) - await session.commit() - - yield user - - -@pytest.fixture -async def regular_user(test_db, rbac_data): - from fastapi_users.password import PasswordHelper - - password_helper = PasswordHelper() - hashed_password = password_helper.hash("user123") - - async with test_db() as session: - user = User( - username="regularuser", - email="user@example.com", - hashed_password=hashed_password, - is_active=True, - is_verified=True, - is_superuser=False, - ) - session.add(user) - await session.commit() - await session.refresh(user) - - user_role = UserRole(user_id=user.id, role_id=2) - session.add(user_role) - await session.commit() - - yield user - - -@pytest.fixture -async def superuser_user(test_db, rbac_data): - from fastapi_users.password import PasswordHelper - - password_helper = PasswordHelper() - hashed_password = password_helper.hash("super123") - - async with test_db() as session: - user = User( - username="superuser", - email="super@example.com", - hashed_password=hashed_password, - is_active=True, - is_verified=True, - is_superuser=True, - ) - session.add(user) - await session.commit() - await session.refresh(user) - - yield user - - @pytest.fixture async def rbac_audit_client( test_db, local_cache, admin_user, regular_user, superuser_user diff --git a/tests/integration/test_rbac.py b/tests/integration/test_rbac.py index 65de85a..27b0835 100644 --- a/tests/integration/test_rbac.py +++ b/tests/integration/test_rbac.py @@ -3,115 +3,9 @@ from httpx import ASGITransport, AsyncClient from src.auth import owner_or_perm, require_permissions, require_roles -from src.auth.models import User -from src.auth.rbac.models import Permission, RolePermission, UserRole from src.shared.errors import ErrorCode -@pytest.fixture -async def rbac_data(test_db): - async with test_db() as session: - perm_read = Permission(id=1, code="user:read", name="Read Users", module="user") - perm_write = Permission( - id=2, code="user:write", name="Write Users", module="user" - ) - perm_delete = Permission( - id=3, code="user:delete", name="Delete Users", module="user" - ) - perm_admin_all = Permission( - id=4, code="admin:*", name="Admin All", module="admin" - ) - session.add_all([perm_read, perm_write, perm_delete, perm_admin_all]) - await session.commit() - - role_perms = [ - RolePermission(role_id=1, permission_id=1), - RolePermission(role_id=1, permission_id=2), - RolePermission(role_id=1, permission_id=3), - RolePermission(role_id=1, permission_id=4), - RolePermission(role_id=2, permission_id=1), - ] - session.add_all(role_perms) - await session.commit() - - -@pytest.fixture -async def admin_user(test_db, rbac_data): - from fastapi_users.password import PasswordHelper - - password_helper = PasswordHelper() - hashed_password = password_helper.hash("admin123") - - async with test_db() as session: - user = User( - username="adminuser", - email="admin@example.com", - hashed_password=hashed_password, - is_active=True, - is_verified=True, - is_superuser=False, - ) - session.add(user) - await session.commit() - await session.refresh(user) - - user_role = UserRole(user_id=user.id, role_id=1) - session.add(user_role) - await session.commit() - - yield user - - -@pytest.fixture -async def regular_user(test_db, rbac_data): - from fastapi_users.password import PasswordHelper - - password_helper = PasswordHelper() - hashed_password = password_helper.hash("user123") - - async with test_db() as session: - user = User( - username="regularuser", - email="user@example.com", - hashed_password=hashed_password, - is_active=True, - is_verified=True, - is_superuser=False, - ) - session.add(user) - await session.commit() - await session.refresh(user) - - user_role = UserRole(user_id=user.id, role_id=2) - session.add(user_role) - await session.commit() - - yield user - - -@pytest.fixture -async def superuser_user(test_db, rbac_data): - from fastapi_users.password import PasswordHelper - - password_helper = PasswordHelper() - hashed_password = password_helper.hash("super123") - - async with test_db() as session: - user = User( - username="superuser", - email="super@example.com", - hashed_password=hashed_password, - is_active=True, - is_verified=True, - is_superuser=True, - ) - session.add(user) - await session.commit() - await session.refresh(user) - - yield user - - @pytest.fixture async def rbac_client(test_db, local_cache, admin_user, regular_user, superuser_user): from src.http.routers import auth_router diff --git a/tests/shared/test_mixins.py b/tests/shared/test_mixins.py index db80e34..721e1a8 100644 --- a/tests/shared/test_mixins.py +++ b/tests/shared/test_mixins.py @@ -25,10 +25,12 @@ async def mixin_db(): async with engine.begin() as conn: await conn.run_sync(SQLModel.metadata.create_all) - yield async_session - - async with engine.begin() as conn: - await conn.run_sync(SQLModel.metadata.drop_all) + try: + yield async_session + finally: + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.drop_all) + await engine.dispose() async def test_timestamp_mixin_updated_at_updates(mixin_db): From 00ff05d5e5bcdc2234ddc1e8f66530aea14c5705 Mon Sep 17 00:00:00 2001 From: Baiheng Xie <874256269@qq.com> Date: Thu, 5 Mar 2026 16:07:35 +0800 Subject: [PATCH 2/5] fix: required jwt secret length --- .env-example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env-example b/.env-example index 9bccd5a..9ee6a53 100644 --- a/.env-example +++ b/.env-example @@ -25,7 +25,7 @@ CACHE__DB=0 CACHE__PASSWORD= CACHE__ENCODING=utf-8 -AUTH__JWT_SECRET=your-secret-key-here +AUTH__JWT_SECRET=your-super-super-long-secret-key-here AUTH__JWT_ALGORITHM=HS256 AUTH__JWT_LIFETIME_SECONDS=1800 AUTH__REFRESH_TOKEN_LIFETIME_SECONDS=2592000 From cac47dbf7096769a571efababb040cf163cc15f7 Mon Sep 17 00:00:00 2001 From: Baiheng Xie <874256269@qq.com> Date: Thu, 5 Mar 2026 23:30:46 +0800 Subject: [PATCH 3/5] fix: session level of fixture_loop_scope --- pyproject.toml | 3 ++- tests/conftest.py | 39 +++++++++++++++++++++++++-------------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a6cab5d..e0a4c45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,8 @@ packages = ["src"] [tool.pytest.ini_options] asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" +asyncio_default_fixture_loop_scope = "session" +asyncio_default_test_loop_scope = "session" # filterwarnings = ["error::DeprecationWarning"] timeout = 5 pythonpath = ["."] diff --git a/tests/conftest.py b/tests/conftest.py index 0569e81..97f3b04 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,13 +3,14 @@ import dotenv import pytest +import pytest_asyncio from fakeredis import FakeAsyncRedis from fastapi import FastAPI from httpx import ASGITransport, AsyncClient from loguru import logger from redis.asyncio.client import Redis as AsyncRedisClient -from sqlalchemy import text -from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy import event +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from sqlmodel import SQLModel @@ -80,23 +81,36 @@ async def async_client(full_app: FastAPI) -> AsyncGenerator[AsyncClient, None]: yield client -@pytest.fixture -async def test_db(): +@pytest_asyncio.fixture(scope="session") +async def test_engine(tmp_path_factory) -> AsyncGenerator[AsyncEngine, None]: + db_file = tmp_path_factory.mktemp("db") / "test.db" engine = create_async_engine( - "sqlite+aiosqlite:///:memory:", + f"sqlite+aiosqlite:///{db_file}", echo=False, connect_args={"check_same_thread": False}, ) - async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) - async with engine.connect() as conn: - await conn.execute(text("PRAGMA foreign_keys=ON")) - await conn.commit() + @event.listens_for(engine.sync_engine, "connect") + def _set_sqlite_pragma(dbapi_conn, _): + cursor = dbapi_conn.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + + yield engine + await engine.dispose() + + +@pytest.fixture +async def test_db(test_engine: AsyncEngine): + async_session = sessionmaker( + test_engine, class_=AsyncSession, expire_on_commit=False + ) - async with engine.begin() as conn: + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.drop_all) await conn.run_sync(SQLModel.metadata.create_all) - async with engine.connect() as conn: + async with test_engine.connect() as conn: await insert_rbac_seed_data_async(conn) async def override_get_session(): @@ -108,9 +122,6 @@ async def override_get_session(): try: yield async_session finally: - async with engine.begin() as conn: - await conn.run_sync(SQLModel.metadata.drop_all) - await engine.dispose() app.dependency_overrides.clear() From 12268439b6aa3a5af88b884049e348c91d5d9b80 Mon Sep 17 00:00:00 2001 From: Baiheng Xie <874256269@qq.com> Date: Thu, 5 Mar 2026 23:42:13 +0800 Subject: [PATCH 4/5] fix: remove deprecated parameters from FakeAsyncRedis client --- tests/conftest.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 97f3b04..781661a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,6 @@ from fastapi import FastAPI from httpx import ASGITransport, AsyncClient from loguru import logger -from redis.asyncio.client import Redis as AsyncRedisClient from sqlalchemy import event from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker @@ -23,11 +22,6 @@ dotenv.load_dotenv(dotenv_path=env_path) -class _FakeRedisClientNoDeprecatedParams(AsyncRedisClient): - def __init__(self, *, decode_responses: bool = False, **kwargs): - super().__init__(decode_responses=decode_responses, **kwargs) - - @pytest.fixture def settings(): from src.config import get_settings @@ -135,9 +129,7 @@ async def local_cache(): @pytest.fixture async def redis_cache(): cache = RedisCache(host="localhost", port=6379, db=1) - cache._client = FakeAsyncRedis( - decode_responses=True, client_class=_FakeRedisClientNoDeprecatedParams - ) + cache._client = FakeAsyncRedis(decode_responses=True) await cache.clear() yield cache await cache.clear() From d8cc57f3c99aea4c3dfc14355edf0955a3ef868b Mon Sep 17 00:00:00 2001 From: Baiheng Xie <874256269@qq.com> Date: Fri, 6 Mar 2026 00:06:47 +0800 Subject: [PATCH 5/5] fix(fixture): top level import & yield to outside --- tests/conftest.py | 2 +- tests/integration/conftest.py | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 781661a..83da3a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -94,7 +94,7 @@ def _set_sqlite_pragma(dbapi_conn, _): await engine.dispose() -@pytest.fixture +@pytest.fixture(scope="function") async def test_db(test_engine: AsyncEngine): async_session = sessionmaker( test_engine, class_=AsyncSession, expire_on_commit=False diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 9bacc64..a0ce66e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,4 +1,5 @@ import pytest +from fastapi_users.password import PasswordHelper from src.auth.models import User from src.auth.rbac.models import Permission, RolePermission, UserRole @@ -33,8 +34,6 @@ async def rbac_data(test_db): @pytest.fixture async def admin_user(test_db, rbac_data): - from fastapi_users.password import PasswordHelper - password_helper = PasswordHelper() hashed_password = password_helper.hash("admin123") @@ -55,13 +54,11 @@ async def admin_user(test_db, rbac_data): session.add(user_role) await session.commit() - yield user + yield user @pytest.fixture async def regular_user(test_db, rbac_data): - from fastapi_users.password import PasswordHelper - password_helper = PasswordHelper() hashed_password = password_helper.hash("user123") @@ -82,13 +79,11 @@ async def regular_user(test_db, rbac_data): session.add(user_role) await session.commit() - yield user + yield user @pytest.fixture async def superuser_user(test_db, rbac_data): - from fastapi_users.password import PasswordHelper - password_helper = PasswordHelper() hashed_password = password_helper.hash("super123") @@ -105,4 +100,4 @@ async def superuser_user(test_db, rbac_data): await session.commit() await session.refresh(user) - yield user + yield user