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, SystemLogRepository, 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 system_log_repository(db_session: Session) -> SystemLogRepository: return SystemLogRepository(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