From 5d34b04ca11139a2f4b0f42ad9c7e0c8418de28b Mon Sep 17 00:00:00 2001 From: grillazz Date: Thu, 25 Dec 2025 15:56:23 +0100 Subject: [PATCH 01/11] Add test database configuration and schema creation for testing --- .env | 2 ++ Makefile | 2 +- app/config.py | 13 +++++++++++++ app/database.py | 27 +++++++++++++++++++++++++++ db/Dockerfile | 3 --- pyproject.toml | 4 ++-- test-compose.yml | 9 --------- tests/conftest.py | 39 +++++++++++++++++++++++++++++++++++++-- uv.lock | 14 ++++++++++++++ 9 files changed, 96 insertions(+), 17 deletions(-) delete mode 100644 test-compose.yml diff --git a/.env b/.env index 13f0864..3dbfa84 100644 --- a/.env +++ b/.env @@ -6,6 +6,8 @@ POSTGRES_HOST=postgres POSTGRES_PORT=5432 POSTGRES_DB=devdb POSTGRES_USER=devdb +POSTGRES_TEST_DB=testdb +POSTGRES_TEST_USER=testdb POSTGRES_PASSWORD=secret # Redis diff --git a/Makefile b/Makefile index 892f499..c67520d 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ docker-create-db-migration: ## Create a new alembic database migration. Example # ==================================================================================== .PHONY: docker-test docker-test: ## Run project tests - docker compose -f compose.yml -f test-compose.yml run --rm api1 pytest tests --durations=0 -vv + docker compose -f compose.yml run --rm api1 pytest tests --durations=0 -vv .PHONY: docker-test-snapshot docker-test-snapshot: ## Run project tests and update snapshots diff --git a/app/config.py b/app/config.py index 260ac36..81722b7 100644 --- a/app/config.py +++ b/app/config.py @@ -33,6 +33,8 @@ class Settings(BaseSettings): POSTGRES_PASSWORD: str POSTGRES_HOST: str POSTGRES_DB: str + POSTGRES_TEST_USER: str + POSTGRES_TEST_DB: str @computed_field @property @@ -80,6 +82,17 @@ def asyncpg_url(self) -> PostgresDsn: path=self.POSTGRES_DB, ) + @computed_field + @property + def test_asyncpg_url(self) -> PostgresDsn: + return MultiHostUrl.build( + scheme="postgresql+asyncpg", + username=self.POSTGRES_USER, + password=self.POSTGRES_PASSWORD, + host=self.POSTGRES_HOST, + path=self.POSTGRES_TEST_DB, + ) + @computed_field @property def postgres_url(self) -> PostgresDsn: diff --git a/app/database.py b/app/database.py index 4917d9b..c893572 100644 --- a/app/database.py +++ b/app/database.py @@ -15,6 +15,12 @@ echo=True, ) +test_engine = create_async_engine( + global_settings.test_asyncpg_url.unicode_string(), + future=True, + echo=True, +) + # expire_on_commit=False will prevent attributes from being expired # after commit. AsyncSessionFactory = async_sessionmaker( @@ -23,6 +29,12 @@ expire_on_commit=False, ) +TestAsyncSessionFactory = async_sessionmaker( + test_engine, + autoflush=False, + expire_on_commit=False, +) + # Dependency async def get_db() -> AsyncGenerator: @@ -38,3 +50,18 @@ async def get_db() -> AsyncGenerator: if not isinstance(ex, ResponseValidationError): await logger.aerror(f"Database-related error: {repr(ex)}") raise # Re-raise to be handled by appropriate handlers + + +async def get_test_db() -> AsyncGenerator: + async with TestAsyncSessionFactory() as session: + try: + yield session + await session.commit() + except SQLAlchemyError: + # Re-raise SQLAlchemy errors to be handled by the global handler + raise + except Exception as ex: + # Only log actual database-related issues, not response validation + if not isinstance(ex, ResponseValidationError): + await logger.aerror(f"Database-related error: {repr(ex)}") + raise # Re-raise to be handled by appropriate handlers \ No newline at end of file diff --git a/db/Dockerfile b/db/Dockerfile index 0da0c0a..08170b4 100644 --- a/db/Dockerfile +++ b/db/Dockerfile @@ -1,9 +1,6 @@ # pull official base image FROM postgres:17.6-alpine -# run create.sql on init -ADD create.sql /docker-entrypoint-initdb.d - WORKDIR /home/gx/code COPY shakespeare_chapter.sql /home/gx/code/shakespeare_chapter.sql diff --git a/pyproject.toml b/pyproject.toml index 06bfee5..95dc7d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,8 +40,8 @@ dev-dependencies = [ "ipython>=9.5.0", "sqlacodegen<=3.1.1", "tryceratops>=2.4.1", - "locust>=2.40.5" - + "locust>=2.40.5", + "sqlalchemy-utils>=0.41.1" ] diff --git a/test-compose.yml b/test-compose.yml deleted file mode 100644 index 7a3c6ba..0000000 --- a/test-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - api1: - environment: - - POSTGRES_DB=testdb - - postgres: - environment: - - POSTGRES_USER=${POSTGRES_USER} - - POSTGRES_DB=testdb \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index cee66a1..3f232c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,13 @@ from collections.abc import AsyncGenerator +from types import SimpleNamespace from typing import Any import pytest from httpx import ASGITransport, AsyncClient +from sqlalchemy import text +from sqlalchemy.exc import ProgrammingError -from app.database import engine +from app.database import engine, test_engine, get_test_db, get_db from app.main import app from app.models.base import Base from app.redis import get_redis @@ -19,15 +22,46 @@ def anyio_backend(request): return request.param +def _create_db(conn) -> None: + """Create a database schema if it doesn't exist.""" + try: + conn.execute(text("CREATE DATABASE testdb")) + except ProgrammingError: + # This might be raised by databases that don't support `IF NOT EXISTS` + # and the schema already exists. You can choose to ignore it. + pass + + +def _create_db_schema(conn) -> None: + """Create a database schema if it doesn't exist.""" + try: + conn.execute(text("CREATE SCHEMA happy_hog")) + conn.execute(text("CREATE SCHEMA shakespeare")) + except ProgrammingError: + # This might be raised by databases that don't support `IF NOT EXISTS` + # and the schema already exists. You can choose to ignore it. + pass + @pytest.fixture(scope="session") async def start_db(): - async with engine.begin() as conn: + # The `engine` is configured for the default 'postgres' database. + # We connect to it and create the test database. + # A transaction block is not used, as CREATE DATABASE cannot run inside it. + async with engine.connect() as conn: + await conn.execute(text("COMMIT")) # Ensure we're not in a transaction + await conn.run_sync(_create_db) + + # Now, connect to the newly created `testdb` with `test_engine` + async with test_engine.begin() as conn: + await conn.execute(text("COMMIT")) # Ensure we're not in a transaction + await conn.run_sync(_create_db_schema) await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.create_all) # for AsyncEngine created in function scope, close and # clean-up pooled connections await engine.dispose() + await test_engine.dispose() @pytest.fixture(scope="session") @@ -40,5 +74,6 @@ async def client(start_db) -> AsyncGenerator[AsyncClient, Any]: # noqa: ARG001 headers={"Content-Type": "application/json"}, transport=transport, ) as test_client: + app.dependency_overrides[get_db] = get_test_db app.redis = await get_redis() yield test_client diff --git a/uv.lock b/uv.lock index f1c47e6..41a04ef 100644 --- a/uv.lock +++ b/uv.lock @@ -500,6 +500,7 @@ dev = [ { name = "pyupgrade" }, { name = "ruff" }, { name = "sqlacodegen" }, + { name = "sqlalchemy-utils" }, { name = "tryceratops" }, ] @@ -540,6 +541,7 @@ dev = [ { name = "pyupgrade", specifier = ">=3.20.0" }, { name = "ruff", specifier = ">=0.13.1" }, { name = "sqlacodegen", specifier = "<=3.1.1" }, + { name = "sqlalchemy-utils", specifier = ">=0.41.1" }, { name = "tryceratops", specifier = ">=2.4.1" }, ] @@ -1660,6 +1662,18 @@ asyncio = [ { name = "greenlet" }, ] +[[package]] +name = "sqlalchemy-utils" +version = "0.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/7d/eb9565b6a49426552a5bf5c57e7c239c506dc0e4e5315aec6d1e8241dc7c/sqlalchemy_utils-0.42.1.tar.gz", hash = "sha256:881f9cd9e5044dc8f827bccb0425ce2e55490ce44fc0bb848c55cc8ee44cc02e", size = 130789, upload-time = "2025-12-13T03:14:13.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/25/7400c18c3ee97914cc99c90007795c00a4ec5b60c853b49db7ba24d11179/sqlalchemy_utils-0.42.1-py3-none-any.whl", hash = "sha256:243cfe1b3a1dae3c74118ae633f1d1e0ed8c787387bc33e556e37c990594ac80", size = 91761, upload-time = "2025-12-13T03:14:15.014Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" From 242ea2ab9ae20d298fd2cc58135e800f5db891a5 Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 28 Dec 2025 18:20:49 +0100 Subject: [PATCH 02/11] refactor: update database creation documentation in conftest.py --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3f232c6..21df394 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ def anyio_backend(request): return request.param def _create_db(conn) -> None: - """Create a database schema if it doesn't exist.""" + """Create the test database if it doesn't exist.""" try: conn.execute(text("CREATE DATABASE testdb")) except ProgrammingError: @@ -54,7 +54,7 @@ async def start_db(): # Now, connect to the newly created `testdb` with `test_engine` async with test_engine.begin() as conn: - await conn.execute(text("COMMIT")) # Ensure we're not in a transaction + await conn.run_sync(_create_db_schema) await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.create_all) From 1fe0faa5c9ae6cc19aa69f1b5a48104e634023b8 Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 28 Dec 2025 18:35:19 +0100 Subject: [PATCH 03/11] refactor: enhance database schema creation and add documentation for test database URL --- app/config.py | 14 ++++++++++++++ tests/conftest.py | 6 +++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/config.py b/app/config.py index 81722b7..6f1126e 100644 --- a/app/config.py +++ b/app/config.py @@ -85,6 +85,19 @@ def asyncpg_url(self) -> PostgresDsn: @computed_field @property def test_asyncpg_url(self) -> PostgresDsn: + """ + This is a computed field that generates a PostgresDsn URL for the test database using asyncpg. + + The URL is built using the MultiHostUrl.build method, which takes the following parameters: + - scheme: The scheme of the URL. In this case, it is "postgresql+asyncpg". + - username: The username for the Postgres database, retrieved from the POSTGRES_USER environment variable. + - password: The password for the Postgres database, retrieved from the POSTGRES_PASSWORD environment variable. + - host: The host of the Postgres database, retrieved from the POSTGRES_HOST environment variable. + - path: The path of the Postgres test database, retrieved from the POSTGRES_TEST_DB environment variable. + + Returns: + PostgresDsn: The constructed PostgresDsn URL for the test database with asyncpg. + """ return MultiHostUrl.build( scheme="postgresql+asyncpg", username=self.POSTGRES_USER, @@ -93,6 +106,7 @@ def test_asyncpg_url(self) -> PostgresDsn: path=self.POSTGRES_TEST_DB, ) + @computed_field @property def postgres_url(self) -> PostgresDsn: diff --git a/tests/conftest.py b/tests/conftest.py index 21df394..8d5b600 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,8 +35,9 @@ def _create_db(conn) -> None: def _create_db_schema(conn) -> None: """Create a database schema if it doesn't exist.""" try: - conn.execute(text("CREATE SCHEMA happy_hog")) - conn.execute(text("CREATE SCHEMA shakespeare")) + """Create a database schema if it doesn't exist.""" + conn.execute(text("CREATE SCHEMA IF NOT EXISTS happy_hog")) + conn.execute(text("CREATE SCHEMA IF NOT EXISTS shakespeare")) except ProgrammingError: # This might be raised by databases that don't support `IF NOT EXISTS` # and the schema already exists. You can choose to ignore it. @@ -54,7 +55,6 @@ async def start_db(): # Now, connect to the newly created `testdb` with `test_engine` async with test_engine.begin() as conn: - await conn.run_sync(_create_db_schema) await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.create_all) From 1188734a0f1fc0eda59b94b510ede88ed8e9aca9 Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 28 Dec 2025 18:41:22 +0100 Subject: [PATCH 04/11] refactor: update test fixtures for improved database session management and isolation --- tests/conftest.py | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8d5b600..30765e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from httpx import ASGITransport, AsyncClient from sqlalchemy import text from sqlalchemy.exc import ProgrammingError +from sqlalchemy.ext.asyncio import AsyncSession from app.database import engine, test_engine, get_test_db, get_db from app.main import app @@ -64,16 +65,43 @@ async def start_db(): await test_engine.dispose() -@pytest.fixture(scope="session") -async def client(start_db) -> AsyncGenerator[AsyncClient, Any]: # noqa: ARG001 - transport = ASGITransport( - app=app, - ) +@pytest.fixture(scope="function") +async def db_session(start_db) -> AsyncGenerator[AsyncSession, Any]: + """ + Provide a transactional database session for each test function. + Rolls back changes after the test. + """ + connection = await test_engine.connect() + transaction = await connection.begin() + session = AsyncSession(bind=connection) + + yield session + + await session.close() + await transaction.rollback() + await connection.close() + + +@pytest.fixture(scope="function") +async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, Any]: + """ + Provide a test client for making API requests. + Uses the function-scoped db_session for test isolation. + """ + + def get_test_db_override(): + yield db_session + + app.dependency_overrides[get_db] = get_test_db_override + app.redis = await get_redis() + + transport = ASGITransport(app=app) async with AsyncClient( base_url="http://testserver/v1", headers={"Content-Type": "application/json"}, transport=transport, ) as test_client: - app.dependency_overrides[get_db] = get_test_db - app.redis = await get_redis() yield test_client + + # Clean up dependency overrides + del app.dependency_overrides[get_db] From a24c47ab3fad7ba4f9277a167794682d117b1edc Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 28 Dec 2025 18:43:21 +0100 Subject: [PATCH 05/11] refactor: improve test_get_token by creating user before token request --- tests/api/test_auth.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index 9000ef3..b5584b5 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -37,17 +37,28 @@ async def test_add_user(client: AsyncClient): # TODO: parametrize test with diff urls including 404 and 401 async def test_get_token(client: AsyncClient): - payload = {"email": "joe@grillazz.com", "password": "s1lly"} + # First, create the user required for this test + user_payload = { + "email": "joe@grillazz.com", + "first_name": "Joe", + "last_name": "Garcia", + "password": "s1lly", + } + create_user_response = await client.post("/user/", json=user_payload) + assert create_user_response.status_code == status.HTTP_201_CREATED + + # Now, request the token for the newly created user + token_payload = {"email": "joe@grillazz.com", "password": "s1lly"} response = await client.post( "/user/token", - data=payload, + data=token_payload, headers={"Content-Type": "application/x-www-form-urlencoded"}, ) assert response.status_code == status.HTTP_201_CREATED claimset = jwt.decode( response.json()["access_token"], options={"verify_signature": False} ) - assert claimset["email"] == payload["email"] + assert claimset["email"] == token_payload["email"] assert claimset["expiry"] == IsPositiveFloat() assert claimset["platform"] == "python-httpx/0.28.1" From 7b0c03f7115defd718e9cb3a11b995c49bb825fc Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 28 Dec 2025 18:44:31 +0100 Subject: [PATCH 06/11] refactor: clean up imports and improve db_session fixture in test configuration --- app/config.py | 1 - app/database.py | 2 +- tests/conftest.py | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/config.py b/app/config.py index 6f1126e..c79d0d6 100644 --- a/app/config.py +++ b/app/config.py @@ -106,7 +106,6 @@ def test_asyncpg_url(self) -> PostgresDsn: path=self.POSTGRES_TEST_DB, ) - @computed_field @property def postgres_url(self) -> PostgresDsn: diff --git a/app/database.py b/app/database.py index c893572..7f34e70 100644 --- a/app/database.py +++ b/app/database.py @@ -64,4 +64,4 @@ async def get_test_db() -> AsyncGenerator: # Only log actual database-related issues, not response validation if not isinstance(ex, ResponseValidationError): await logger.aerror(f"Database-related error: {repr(ex)}") - raise # Re-raise to be handled by appropriate handlers \ No newline at end of file + raise # Re-raise to be handled by appropriate handlers diff --git a/tests/conftest.py b/tests/conftest.py index 30765e8..0e17675 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,4 @@ from collections.abc import AsyncGenerator -from types import SimpleNamespace from typing import Any import pytest @@ -8,7 +7,7 @@ from sqlalchemy.exc import ProgrammingError from sqlalchemy.ext.asyncio import AsyncSession -from app.database import engine, test_engine, get_test_db, get_db +from app.database import engine, get_db, test_engine from app.main import app from app.models.base import Base from app.redis import get_redis @@ -23,6 +22,7 @@ def anyio_backend(request): return request.param + def _create_db(conn) -> None: """Create the test database if it doesn't exist.""" try: @@ -66,7 +66,7 @@ async def start_db(): @pytest.fixture(scope="function") -async def db_session(start_db) -> AsyncGenerator[AsyncSession, Any]: +async def db_session() -> AsyncGenerator[AsyncSession, Any]: """ Provide a transactional database session for each test function. Rolls back changes after the test. From 544d798ec7d0c8e3352cdf917af539eb978a5516 Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 28 Dec 2025 18:48:32 +0100 Subject: [PATCH 07/11] refactor: comment out database schema creation steps in build-and-test.yml --- .github/workflows/build-and-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f130b51..d164ccc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -45,9 +45,9 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v5 - - name: Create database schema - run: PGPASSWORD=secret psql -h 127.0.0.1 -d testdb -U panettone -c "CREATE SCHEMA shakespeare; CREATE SCHEMA happy_hog;" +# - uses: actions/checkout@v5 +# - name: Create database schema +# run: PGPASSWORD=secret psql -h 127.0.0.1 -d testdb -U panettone -c "CREATE SCHEMA shakespeare; CREATE SCHEMA happy_hog;" - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 From 7ef6e710256a5526e42e49e548b22969f4461d0b Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 28 Dec 2025 18:50:04 +0100 Subject: [PATCH 08/11] refactor: comment out database schema creation steps in build-and-test.yml --- .github/workflows/build-and-test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d164ccc..611c6a5 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -45,10 +45,6 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: -# - uses: actions/checkout@v5 -# - name: Create database schema -# run: PGPASSWORD=secret psql -h 127.0.0.1 -d testdb -U panettone -c "CREATE SCHEMA shakespeare; CREATE SCHEMA happy_hog;" - - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 with: From ef6f9bc44bfc03c8582bcc6b94c36896ce6a0dbe Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 28 Dec 2025 18:55:32 +0100 Subject: [PATCH 09/11] refactor: update ruff dependency to version 0.14.10 and streamline build steps --- .github/workflows/build-and-test.yml | 8 ++--- pyproject.toml | 2 +- uv.lock | 46 ++++++++++++++-------------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 611c6a5..4212229 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -41,10 +41,12 @@ jobs: POSTGRES_DB: testdb ports: - 5432:5432 - # needed because the postgres container does not provide a health check options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 with: @@ -54,6 +56,4 @@ jobs: run: uv run --frozen ruff check . - name: Test with python ${{ matrix.python-version }} - run: uv run --frozen pytest - - + run: uv run pytest diff --git a/pyproject.toml b/pyproject.toml index 4103253..c78b233 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ [tool.uv] dev-dependencies = [ - "ruff==0.14.9", + "ruff==0.14.10", "devtools[pygments]==0.12.2", "pyupgrade==3.21.2", "ipython==9.8.0", diff --git a/uv.lock b/uv.lock index d9af38e..7c182b9 100644 --- a/uv.lock +++ b/uv.lock @@ -450,7 +450,7 @@ dev = [ { name = "devtools", extras = ["pygments"], specifier = "==0.12.2" }, { name = "ipython", specifier = "==9.8.0" }, { name = "pyupgrade", specifier = "==3.21.2" }, - { name = "ruff", specifier = "==0.14.9" }, + { name = "ruff", specifier = "==0.14.10" }, { name = "tryceratops", specifier = "==2.4.1" }, ] @@ -1185,28 +1185,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" }, - { url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" }, - { url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" }, - { url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" }, - { url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" }, - { url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" }, - { url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" }, - { url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" }, - { url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" }, - { url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" }, - { url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" }, - { url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" }, - { url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" }, - { url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" }, +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, ] [[package]] From adecd854f3e1ecb88f9656250d6258ebbabab762 Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 28 Dec 2025 19:05:43 +0100 Subject: [PATCH 10/11] rollback --- tests/api/test_auth.py | 17 +++------------- tests/conftest.py | 46 +++++++++--------------------------------- 2 files changed, 12 insertions(+), 51 deletions(-) diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index b5584b5..9000ef3 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -37,28 +37,17 @@ async def test_add_user(client: AsyncClient): # TODO: parametrize test with diff urls including 404 and 401 async def test_get_token(client: AsyncClient): - # First, create the user required for this test - user_payload = { - "email": "joe@grillazz.com", - "first_name": "Joe", - "last_name": "Garcia", - "password": "s1lly", - } - create_user_response = await client.post("/user/", json=user_payload) - assert create_user_response.status_code == status.HTTP_201_CREATED - - # Now, request the token for the newly created user - token_payload = {"email": "joe@grillazz.com", "password": "s1lly"} + payload = {"email": "joe@grillazz.com", "password": "s1lly"} response = await client.post( "/user/token", - data=token_payload, + data=payload, headers={"Content-Type": "application/x-www-form-urlencoded"}, ) assert response.status_code == status.HTTP_201_CREATED claimset = jwt.decode( response.json()["access_token"], options={"verify_signature": False} ) - assert claimset["email"] == token_payload["email"] + assert claimset["email"] == payload["email"] assert claimset["expiry"] == IsPositiveFloat() assert claimset["platform"] == "python-httpx/0.28.1" diff --git a/tests/conftest.py b/tests/conftest.py index 0e17675..8d5b600 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,13 @@ from collections.abc import AsyncGenerator +from types import SimpleNamespace from typing import Any import pytest from httpx import ASGITransport, AsyncClient from sqlalchemy import text from sqlalchemy.exc import ProgrammingError -from sqlalchemy.ext.asyncio import AsyncSession -from app.database import engine, get_db, test_engine +from app.database import engine, test_engine, get_test_db, get_db from app.main import app from app.models.base import Base from app.redis import get_redis @@ -22,7 +22,6 @@ def anyio_backend(request): return request.param - def _create_db(conn) -> None: """Create the test database if it doesn't exist.""" try: @@ -65,43 +64,16 @@ async def start_db(): await test_engine.dispose() -@pytest.fixture(scope="function") -async def db_session() -> AsyncGenerator[AsyncSession, Any]: - """ - Provide a transactional database session for each test function. - Rolls back changes after the test. - """ - connection = await test_engine.connect() - transaction = await connection.begin() - session = AsyncSession(bind=connection) - - yield session - - await session.close() - await transaction.rollback() - await connection.close() - - -@pytest.fixture(scope="function") -async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, Any]: - """ - Provide a test client for making API requests. - Uses the function-scoped db_session for test isolation. - """ - - def get_test_db_override(): - yield db_session - - app.dependency_overrides[get_db] = get_test_db_override - app.redis = await get_redis() - - transport = ASGITransport(app=app) +@pytest.fixture(scope="session") +async def client(start_db) -> AsyncGenerator[AsyncClient, Any]: # noqa: ARG001 + transport = ASGITransport( + app=app, + ) async with AsyncClient( base_url="http://testserver/v1", headers={"Content-Type": "application/json"}, transport=transport, ) as test_client: + app.dependency_overrides[get_db] = get_test_db + app.redis = await get_redis() yield test_client - - # Clean up dependency overrides - del app.dependency_overrides[get_db] From 88a66b1d92804070b7567d19864c14a7ec311539 Mon Sep 17 00:00:00 2001 From: grillazz Date: Sun, 28 Dec 2025 19:07:46 +0100 Subject: [PATCH 11/11] refactor: remove unused import from conftest.py --- tests/conftest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8d5b600..107d3bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,4 @@ from collections.abc import AsyncGenerator -from types import SimpleNamespace from typing import Any import pytest @@ -7,7 +6,7 @@ from sqlalchemy import text from sqlalchemy.exc import ProgrammingError -from app.database import engine, test_engine, get_test_db, get_db +from app.database import engine, get_db, get_test_db, test_engine from app.main import app from app.models.base import Base from app.redis import get_redis