from pathlib import Path import pytest from cryptography.fernet import Fernet from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import StaticPool from app.config import Settings from app.db.models import Activity, Base, HealthState from app.db.repositories import ActivityRepository, SyncRunRepository, UserRepository from app.main import create_app @pytest.fixture def db_session() -> Session: engine = create_engine( "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) Base.metadata.create_all(engine) factory = sessionmaker(bind=engine, expire_on_commit=False) try: with factory() as session: yield session finally: engine.dispose() @pytest.fixture def user_repository(db_session: Session) -> UserRepository: return UserRepository(db_session) @pytest.fixture def activity_repository(db_session: Session) -> ActivityRepository: return ActivityRepository(db_session) @pytest.fixture def sync_run_repository(db_session: Session) -> SyncRunRepository: return SyncRunRepository(db_session) @pytest.fixture def app(tmp_path: Path): settings = Settings( ADMIN_PASSWORD="admin-secret", SECRET_KEY="0123456789abcdef0123456789abcdef", CREDENTIAL_ENCRYPTION_KEY=Fernet.generate_key().decode("ascii"), DATA_DIR=str(tmp_path), DATABASE_URL=f"sqlite:///{tmp_path / 'app.db'}", SYNC_INTERVAL_MINUTES=5, ) application = create_app(settings) try: yield application finally: application.state.db_engine.dispose() @pytest.fixture def client(app) -> TestClient: return TestClient(app) def _extract_csrf(html: str) -> str: marker = 'name="csrf_token" value="' start = html.index(marker) + len(marker) end = html.index('"', start) return html[start:end] class FakeSyncManager: def __init__(self) -> None: self.user_calls: list[int] = [] self.all_calls = 0 self.raise_already_running = False self.mfa_calls: list[tuple[int, str]] = [] async def sync_user(self, user_id: int, mfa_code: str | None = None): if self.raise_already_running: from app.sync.manager import SyncAlreadyRunning raise SyncAlreadyRunning(f"sync already running for user {user_id}") self.user_calls.append(user_id) if mfa_code is not None: self.mfa_calls.append((user_id, mfa_code)) from app.sync.states import SyncOutcome return SyncOutcome(user_id=user_id, status="success", discovered=0, imported=0, skipped=0, failed=0) async def sync_all_enabled(self): self.all_calls += 1 return [] @pytest.fixture def fake_sync_manager() -> FakeSyncManager: return FakeSyncManager() @pytest.fixture def authenticated_client(app, client: TestClient, fake_sync_manager: FakeSyncManager) -> TestClient: page = client.get("/login") csrf = _extract_csrf(page.text) response = client.post("/login", data={"password": "admin-secret", "csrf_token": csrf}, follow_redirects=False) assert response.status_code == 303 app.state.sync_manager = fake_sync_manager client.csrf_token = csrf return client @pytest.fixture def seeded_activity(db_session: Session, user_repository: UserRepository, activity_repository: ActivityRepository) -> Activity: user = user_repository.create( name="Test User", enabled=True, health_state=HealthState.HEALTHY, mywhoosh_email_enc="test@example.com", mywhoosh_password_enc="password", garmin_email_enc="test@garmin.com", garmin_password_enc="garmin_password", ) activity, _ = activity_repository.get_or_create_discovered( user_id=user.id, mywhoosh_activity_id="mw-test-123", activity_name="Test Activity", activity_timestamp=None, ) return activity