""" Shared pytest fixtures for the Apl_GS test suite. Provides: - async_engine: in-memory SQLite engine for isolation - async_session: transactional session rolled back after each test - sample_visit / sample_checklist / sample_photo / sample_outlet / sample_assessment """ import asyncio from datetime import datetime, timezone from typing import AsyncGenerator import pytest import pytest_asyncio from sqlalchemy.ext.asyncio import ( AsyncSession, async_sessionmaker, create_async_engine, ) # Import all models so SQLAlchemy registers them before create_all from models.base import Base import models.visit_model # noqa: F401 import models.checklist_model # noqa: F401 import models.photo_model # noqa: F401 import models.assessment_model # noqa: F401 import models.outlet_model # noqa: F401 from models.visit_model import Visit from models.checklist_model import Checklist from models.photo_model import Photo from models.assessment_model import Assessment from models.outlet_model import Outlet # ── In-memory async SQLite engine ───────────────────────────────────────── TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:" @pytest.fixture(scope="session") def event_loop(): """Use a single event loop for the whole session.""" loop = asyncio.new_event_loop() yield loop loop.close() @pytest_asyncio.fixture(scope="session") async def async_engine(): """Create engine + tables once per session.""" engine = create_async_engine( TEST_DATABASE_URL, connect_args={"check_same_thread": False}, ) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await engine.dispose() @pytest_asyncio.fixture async def async_session(async_engine) -> AsyncGenerator[AsyncSession, None]: """Provide a transactional session that rolls back after each test.""" session_factory = async_sessionmaker( async_engine, expire_on_commit=False ) async with session_factory() as session: async with session.begin(): yield session await session.rollback() # ── Sample data factories ────────────────────────────────────────────────── @pytest_asyncio.fixture async def sample_outlet(async_session: AsyncSession) -> Outlet: """Persist and return a sample Outlet.""" outlet = Outlet( name="Outlet Gedung A", location="Jl. Sudirman No. 1", description="Outlet pusat", ) async_session.add(outlet) await async_session.flush() return outlet @pytest_asyncio.fixture async def sample_visit(async_session: AsyncSession, sample_outlet: Outlet) -> Visit: """Persist and return a sample Visit linked to sample_outlet.""" visit = Visit( title="Test Site Inspection", description="Monthly check", location="Gedung A", visit_date=datetime(2026, 5, 1, 9, 0, tzinfo=timezone.utc), status="pending", outlet_id=sample_outlet.id, ghost_shopper="Auditor Test", visit_type="dine_in", ) async_session.add(visit) await async_session.flush() return visit @pytest_asyncio.fixture async def sample_checklist( async_session: AsyncSession, sample_visit: Visit ) -> Checklist: """Persist and return a sample Checklist item linked to sample_visit.""" item = Checklist( visit_id=sample_visit.id, item_name="Panel listrik", is_checked=False, ) async_session.add(item) await async_session.flush() return item @pytest_asyncio.fixture async def sample_photo( async_session: AsyncSession, sample_visit: Visit ) -> Photo: """Persist and return a sample Photo linked to sample_visit.""" photo = Photo( visit_id=sample_visit.id, file_path="/uploads/test.jpg", file_name="test.jpg", file_size=102400, mime_type="image/jpeg", ) async_session.add(photo) await async_session.flush() return photo @pytest_asyncio.fixture async def sample_assessment( async_session: AsyncSession, sample_visit: Visit ) -> Assessment: """Persist and return a sample Assessment item linked to sample_visit.""" assessment = Assessment( visit_id=sample_visit.id, category="A", item_no=1, criteria="Welcoming Kasir (Senyum, Sapa, Salam)", score=4.0, raw_value="4", notes="Baik", ) async_session.add(assessment) await async_session.flush() return assessment