from pathlib import Path from uuid import uuid4 import pytest from advanced_alchemy.base import UUIDAuditBase from litestar.testing import AsyncTestClient from sqlalchemy import text from sqlalchemy.ext.asyncio import ( AsyncSession, AsyncEngine, async_sessionmaker, create_async_engine, ) from sqlalchemy.engine import URL from sqlalchemy.pool import NullPool from chitai.database import models as m from chitai.config import settings from collections.abc import AsyncGenerator from litestar import Litestar from pytest_databases.docker.postgres import PostgresService from sqlalchemy.orm import selectinload pytest_plugins = [ "tests.data_fixtures", "pytest_databases.docker", "pytest_databases.docker.postgres", ] # Set the environment to use the testing database # os.environ.update( # { # "POSTGRES_DB": "chitai_testing" # } # ) @pytest.fixture(autouse=True) def _patch_settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(settings, "book_cover_path", f"{tmp_path}/covers") @pytest.fixture(name="engine") async def fx_engine( postgres_service: PostgresService, ) -> AsyncGenerator[AsyncEngine, None]: """PostgreSQL instance for testing""" engine = create_async_engine( URL( drivername="postgresql+asyncpg", username=postgres_service.user, password=postgres_service.password, host=postgres_service.host, port=postgres_service.port, database=postgres_service.database, query={}, # type:ignore[arg-type] ), echo=False, poolclass=NullPool, ) # Add pg_trgm extension async with engine.begin() as conn: await conn.execute(text("CREATE EXTENSION IF NOT EXISTS pg_trgm")) # Create all tables metadata = UUIDAuditBase.registry.metadata async with engine.begin() as conn: await conn.run_sync(metadata.create_all) yield engine # Clean up async with engine.begin() as conn: await conn.run_sync(metadata.drop_all) await engine.dispose() @pytest.fixture(name="sessionmaker") def fx_sessionmaker(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]: """Create sessionmaker factory.""" return async_sessionmaker(bind=engine, expire_on_commit=False) @pytest.fixture async def session( sessionmaker: async_sessionmaker[AsyncSession], ) -> AsyncGenerator[AsyncSession, None]: """Create database session for tests.""" async with sessionmaker() as session: yield session await session.rollback() await session.close() @pytest.fixture async def app() -> Litestar: """Create Litestar app for testing.""" from chitai.app import create_app return create_app() @pytest.fixture async def client(app: Litestar) -> AsyncGenerator[AsyncTestClient, None]: """Create test client.""" async with AsyncTestClient(app=app) as client: yield client @pytest.fixture async def authenticated_client( client: AsyncTestClient, test_user: m.User ) -> AsyncTestClient: """Create authenticated test client.""" # login and set auth headers login_response = await client.post( "access/login", data={"email": test_user.email, "password": "password123"} ) assert login_response.status_code == 201 result = login_response.json() token = result["access_token"] client.headers.update({"Authorization": f"Bearer {token}"}) return client @pytest.fixture async def other_authenticated_client( app: Litestar, test_user2: m.User ) -> AsyncGenerator[AsyncTestClient, None]: """Create second authenticated test client as different user.""" async with AsyncTestClient(app=app) as other_client: login_response = await other_client.post( "access/login", data={"email": test_user2.email, "password": "password234"} ) assert login_response.status_code == 201 result = login_response.json() token = result["access_token"] other_client.headers.update({"Authorization": f"Bearer {token}"}) yield other_client # Service fixtures from chitai import services @pytest.fixture async def user_service( sessionmaker: async_sessionmaker[AsyncSession], ) -> AsyncGenerator[services.UserService, None]: """Create UserService instance.""" async with sessionmaker() as session: async with services.UserService.new(session) as service: yield service @pytest.fixture async def library_service( sessionmaker: async_sessionmaker[AsyncSession], ) -> AsyncGenerator[services.LibraryService, None]: """Create LibraryService instance.""" async with sessionmaker() as session: async with services.LibraryService.new(session) as service: yield service @pytest.fixture async def books_service( sessionmaker: async_sessionmaker[AsyncSession], ) -> AsyncGenerator[services.BookService, None]: """Create BookService instance.""" async with sessionmaker() as session: async with services.BookService.new( session, load=[ selectinload(m.Book.author_links).selectinload(m.BookAuthorLink.author), selectinload(m.Book.tag_links).selectinload(m.BookTagLink.tag), m.Book.publisher, m.Book.files, m.Book.identifiers, m.Book.series, ], ) as service: yield service @pytest.fixture async def bookshelf_service( sessionmaker: async_sessionmaker[AsyncSession], ) -> AsyncGenerator[services.ShelfService]: """Create ShelfService instance.""" async with sessionmaker() as session: async with services.ShelfService.new(session) as service: yield service # Data fixtures @pytest.fixture async def test_user(session: AsyncSession) -> AsyncGenerator[m.User, None]: """Create a test user.""" unique_id = str(uuid4())[:8] user = m.User( email=f"user{unique_id}@example.com", password="password123", ) session.add(user) await session.commit() await session.refresh(user) yield user @pytest.fixture async def test_user2(session: AsyncSession) -> AsyncGenerator[m.User, None]: """Create another test user.""" unique_id = str(uuid4())[:8] user = m.User( email=f"user{unique_id}@example.com", password="password234", ) session.add(user) await session.commit() await session.refresh(user) yield user @pytest.fixture async def test_library( session: AsyncSession, tmp_path: Path ) -> AsyncGenerator[m.Library, None]: """Create a test library.""" library = m.Library( name="Testing Library", slug="testing-library", root_path=str(tmp_path), path_template="{author_name}/{title}.{ext}", read_only=False, ) session.add(library) await session.commit() await session.refresh(library) yield library