"""Pytest configuration and fixtures. This module contains shared fixtures and configuration for all tests. """ import sys import os import pytest import pytest_asyncio # Add src to path for importing in tests sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) # Markers for test organization pytest_plugins = ['pytest_asyncio'] def pytest_configure(config): """Configure pytest with custom markers.""" config.addinivalue_line("markers", "unit: Unit tests (no external dependencies)") config.addinivalue_line("markers", "integration: Integration tests (with mocked dependencies)") config.addinivalue_line("markers", "e2e: End-to-end tests (full workflow)") config.addinivalue_line("markers", "slow: Slow tests (skip in quick mode)") @pytest.fixture(scope='session') def project_root(): """Return the project root directory.""" return os.path.dirname(os.path.dirname(__file__)) @pytest.fixture(scope='session') def src_path(project_root): """Return the src directory path.""" return os.path.join(project_root, 'src') @pytest.fixture def temp_dir(tmp_path): """Provide a temporary directory for tests.""" return tmp_path @pytest.fixture def mock_env_vars(monkeypatch): """Set up mock environment variables for testing.""" monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') monkeypatch.setenv('DATABASE_URL', 'sqlite:///./test.db') monkeypatch.setenv('DEBUG', 'true') monkeypatch.setenv('LOG_LEVEL', 'DEBUG') @pytest.fixture def mock_db(): """Create a mock database session for unit tests.""" from unittest.mock import MagicMock return MagicMock() @pytest.fixture def mock_user(): """Create a mock authenticated user for testing.""" from unittest.mock import MagicMock user = MagicMock() user.id = 1 user.email = "test@example.com" user.is_active = True return user @pytest.fixture def mock_encryption_service(): """Create a mock encryption service for testing.""" from unittest.mock import MagicMock mock = MagicMock() mock.encrypt.return_value = "encrypted_key_value" mock.decrypt.return_value = "sk-or-v1-decrypted" return mock @pytest.fixture def client(): """Create a test client with fresh database.""" from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool from openrouter_monitor.database import Base, get_db from openrouter_monitor.main import app # Setup in-memory test database SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}, poolclass=StaticPool, ) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def override_get_db(): """Override get_db dependency for testing.""" try: db = TestingSessionLocal() yield db finally: db.close() app.dependency_overrides[get_db] = override_get_db Base.metadata.create_all(bind=engine) with TestClient(app) as c: yield c Base.metadata.drop_all(bind=engine) @pytest.fixture def db_session(client): """Get database session from client dependency override.""" from openrouter_monitor.database import get_db from openrouter_monitor.main import app # Get the override function override = app.dependency_overrides.get(get_db) if override: db = next(override()) yield db db.close() else: # Fallback - create new session from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool from openrouter_monitor.database import Base engine = create_engine( "sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base.metadata.create_all(bind=engine) db = SessionLocal() yield db db.close() @pytest.fixture def auth_headers(client): """Create a user and return JWT auth headers.""" from openrouter_monitor.models import User # Create test user via API user_data = { "email": "testuser@example.com", "password": "TestPassword123!" } # Register user response = client.post("/api/auth/register", json=user_data) if response.status_code == 400: # User might already exist pass # Login to get token response = client.post("/api/auth/login", json=user_data) if response.status_code == 200: token = response.json()["access_token"] return {"Authorization": f"Bearer {token}"} # Fallback - create token directly # Get user from db from openrouter_monitor.database import get_db from openrouter_monitor.main import app from openrouter_monitor.services.jwt import create_access_token override = app.dependency_overrides.get(get_db) if override: db = next(override()) user = db.query(User).filter(User.email == user_data["email"]).first() if user: token = create_access_token(data={"sub": str(user.id)}) return {"Authorization": f"Bearer {token}"} return {} @pytest.fixture def authorized_client(client, auth_headers): """Create an authorized test client with JWT token.""" # Return client with auth headers pre-configured original_get = client.get original_post = client.post original_put = client.put original_delete = client.delete def auth_get(url, **kwargs): headers = kwargs.pop("headers", {}) headers.update(auth_headers) return original_get(url, headers=headers, **kwargs) def auth_post(url, **kwargs): headers = kwargs.pop("headers", {}) headers.update(auth_headers) return original_post(url, headers=headers, **kwargs) def auth_put(url, **kwargs): headers = kwargs.pop("headers", {}) headers.update(auth_headers) return original_put(url, headers=headers, **kwargs) def auth_delete(url, **kwargs): headers = kwargs.pop("headers", {}) headers.update(auth_headers) return original_delete(url, headers=headers, **kwargs) client.get = auth_get client.post = auth_post client.put = auth_put client.delete = auth_delete yield client # Restore original methods client.get = original_get client.post = original_post client.put = original_put client.delete = original_delete