diff --git a/app/core/dependencies.py b/app/core/dependencies.py new file mode 100644 index 0000000..9e5290d --- /dev/null +++ b/app/core/dependencies.py @@ -0,0 +1,15 @@ +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession +from app.db.session import get_db +from app.repositories.job_repository import JobRepository +from app.services.job_service import JobService + + +def get_job_repository(db: AsyncSession = Depends(get_db)) -> JobRepository: + """Dependency to retrieve JobRepository""" + return JobRepository(db) + + +def get_job_service(repo: JobRepository = Depends(get_job_repository)) -> JobService: + """Dependency to retrieve JobService""" + return JobService(repo) diff --git a/app/core/exceptions.py b/app/core/exceptions.py index f44b4d9..e58ceab 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -1,11 +1,20 @@ from fastapi import HTTPException, status + + class AlemnoException(Exception): + """Base exception for Alemno Payments Application""" pass + + class JobNotFoundException(AlemnoException): + """Raised when a job is not found""" def __init__(self, job_id: str): self.job_id = job_id super().__init__(f"Job with ID {job_id} not found") + + class InvalidCSVException(AlemnoException): + """Raised when the uploaded CSV is invalid""" def __init__(self, detail: str): self.detail = detail super().__init__(detail) diff --git a/tests/conftest.py b/tests/conftest.py index 6a6254f..a46deaf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,11 +3,15 @@ from typing import AsyncGenerator import pytest from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession + from app.core.config import settings from app.core.dependencies import get_db +from app.db.base import Base from app.main import app + # Use a test database suffix or a separate database for testing TEST_DATABASE_URL = settings.DATABASE_URL.replace(settings.POSTGRES_DB, f"test_{settings.POSTGRES_DB}") + # Create async engine for test db test_engine = create_async_engine( TEST_DATABASE_URL, @@ -58,3 +62,39 @@ def initialize_test_db(): loop.run_until_complete(teardown_db()) except Exception: pass + + +@pytest.fixture +async def db_session() -> AsyncGenerator[AsyncSession, None]: + """Provide a transactional test database session.""" + if _db_offline: + pytest.skip("PostgreSQL database is offline") + return + + async with TestAsyncSessionLocal() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() + + +@pytest.fixture +async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]: + """Provide an HTTPX AsyncClient for FastAPI endpoint testing with db overrides.""" + # Override get_db dependency to use the test session + async def override_get_db() -> AsyncGenerator[AsyncSession, None]: + yield db_session + + app.dependency_overrides[get_db] = override_get_db + + async with AsyncClient( + transport=ASGITransport(app=app), + base_url="http://test", + ) as client: + yield client + + app.dependency_overrides.clear() diff --git a/tests/repositories/test_job_repository.py b/tests/repositories/test_job_repository.py new file mode 100644 index 0000000..b370a07 --- /dev/null +++ b/tests/repositories/test_job_repository.py @@ -0,0 +1,56 @@ +import pytest +from sqlalchemy.ext.asyncio import AsyncSession +from app.repositories.job_repository import JobRepository +from app.db.models.job import Job + +# Set up asyncio marker +pytestmark = pytest.mark.asyncio + + +async def test_create_and_get_job(db_session: AsyncSession): + repo = JobRepository(db_session) + + # Create job + job = await repo.create(filename="test_transactions.csv") + assert job.id is not None + assert job.status == "pending" + assert job.filename == "test_transactions.csv" + + # Retrieve job + fetched_job = await repo.get_by_id(job.id) + assert fetched_job is not None + assert fetched_job.id == job.id + assert fetched_job.filename == "test_transactions.csv" + + +async def test_update_job(db_session: AsyncSession): + repo = JobRepository(db_session) + job = await repo.create(filename="update_test.csv") + + # Update status and count + updated = await repo.update(job, status="completed", row_count=10, summary={"total": 100}) + assert updated.status == "completed" + assert updated.row_count == 10 + assert updated.summary == {"total": 100} + + # Retrieve to verify persistence + fetched = await repo.get_by_id(job.id) + assert fetched.status == "completed" + assert fetched.row_count == 10 + + +async def test_list_jobs(db_session: AsyncSession): + repo = JobRepository(db_session) + + # Clear existing if any + await repo.create(filename="list_1.csv") + await repo.create(filename="list_2.csv") + + jobs = await repo.list_all() + assert len(jobs) >= 2 + + pending_jobs = await repo.list_all(status="pending") + assert len(pending_jobs) >= 2 + + completed_jobs = await repo.list_all(status="completed") + assert len(completed_jobs) == 1 # From the update test above