""" Fixtures partagées pour les tests backend. Ce fichier configure: - Base de données SQLite in-memory pour isolation totale - Client HTTP async avec override des dépendances - Mocks pour services externes (Ansible, ntfy, WebSocket) - Factories pour créer des données de test """ import asyncio import os from datetime import datetime, timezone from typing import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio from httpx import ASGITransport, AsyncClient from sqlalchemy import event from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker from sqlalchemy.pool import StaticPool # Set test environment before imports os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///:memory:" os.environ["API_KEY"] = "test-api-key-12345" os.environ["JWT_SECRET_KEY"] = "test-jwt-secret-key-for-testing-only" os.environ["NTFY_ENABLED"] = "false" os.environ["ANSIBLE_DIR"] = "." from app.models.database import Base from app.core.dependencies import get_db, verify_api_key, get_current_user from app import create_app # ============================================================================ # DATABASE FIXTURES # ============================================================================ @pytest.fixture(scope="session") def event_loop() -> Generator: """Create event loop for session-scoped async fixtures.""" loop = asyncio.new_event_loop() yield loop loop.close() @pytest_asyncio.fixture async def async_engine(): """Create async engine with in-memory SQLite.""" engine = create_async_engine( "sqlite+aiosqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, echo=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 db_session(async_engine) -> AsyncGenerator[AsyncSession, None]: """Provide a transactional database session for each test.""" async_session_factory = async_sessionmaker( async_engine, class_=AsyncSession, expire_on_commit=False, autoflush=False, ) async with async_session_factory() as session: yield session await session.rollback() # ============================================================================ # APP & CLIENT FIXTURES # ============================================================================ @pytest_asyncio.fixture async def app(db_session: AsyncSession): """Create FastAPI app with overridden dependencies.""" application = create_app() # Override database dependency async def override_get_db(): yield db_session # Override auth to always pass async def override_verify_api_key(): return True async def override_get_current_user(): return { "type": "test", "authenticated": True, "user_id": "test-user-id", "username": "testuser", "role": "admin", } application.dependency_overrides[get_db] = override_get_db application.dependency_overrides[verify_api_key] = override_verify_api_key application.dependency_overrides[get_current_user] = override_get_current_user yield application application.dependency_overrides.clear() @pytest_asyncio.fixture async def client(app) -> AsyncGenerator[AsyncClient, None]: """Provide async HTTP client for testing endpoints.""" transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac @pytest_asyncio.fixture async def unauthenticated_client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]: """Provide async HTTP client without auth overrides.""" application = create_app() async def override_get_db(): yield db_session application.dependency_overrides[get_db] = override_get_db transport = ASGITransport(app=application) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac application.dependency_overrides.clear() # ============================================================================ # SERVICE MOCKS # ============================================================================ @pytest.fixture def mock_ansible_service(): """Mock AnsibleService to avoid real subprocess calls.""" with patch("app.services.ansible_service.ansible_service") as mock: mock.get_playbooks.return_value = [ { "name": "health-check", "filename": "health-check.yml", "path": "/playbooks/health-check.yml", "category": "monitoring", "subcategory": "other", "hosts": "all", "size": 1024, "modified": datetime.now(timezone.utc).isoformat(), "description": "Check host health", } ] mock.get_hosts_from_inventory.return_value = [] mock.get_groups.return_value = ["env_prod", "env_dev", "role_web"] mock.get_env_groups.return_value = ["env_prod", "env_dev"] mock.get_role_groups.return_value = ["role_web"] mock.execute_playbook = AsyncMock(return_value={ "success": True, "return_code": 0, "stdout": "PLAY RECAP\nok=1 changed=0 failed=0", "stderr": "", "execution_time": 1.5, "playbook": "health-check.yml", "target": "all", "check_mode": False, }) mock.invalidate_cache = MagicMock() mock.host_exists.return_value = False mock.add_host_to_inventory = MagicMock() mock.remove_host_from_inventory = MagicMock() mock.update_host_groups = MagicMock() yield mock @pytest.fixture def mock_ws_manager(): """Mock WebSocket manager.""" with patch("app.services.websocket_service.ws_manager") as mock: mock.broadcast = AsyncMock() mock.connect = AsyncMock() mock.disconnect = MagicMock() mock.connection_count = 0 yield mock @pytest.fixture def mock_notification_service(): """Mock notification service to avoid real HTTP calls.""" with patch("app.services.notification_service.notification_service") as mock: mock.enabled = False mock.send = AsyncMock(return_value=True) mock.send_request = AsyncMock() mock.notify_task_completed = AsyncMock(return_value=True) mock.notify_task_failed = AsyncMock(return_value=True) yield mock @pytest.fixture def mock_scheduler_service(): """Mock scheduler service.""" with patch("app.services.scheduler_service.scheduler_service") as mock: mock._started = False mock.start_async = AsyncMock() mock.shutdown = MagicMock() mock.get_all_schedules.return_value = [] mock.get_schedule.return_value = None mock.add_schedule_to_cache = MagicMock() mock.remove_schedule_from_cache = MagicMock() mock.validate_cron_expression.return_value = { "valid": True, "expression": "0 2 * * *", "next_runs": [], "error": None, } yield mock # ============================================================================ # DATA FACTORIES # ============================================================================ class HostFactory: """Factory for creating test hosts.""" _counter = 0 @classmethod def build(cls, **kwargs) -> dict: cls._counter += 1 return { "id": kwargs.get("id", f"host-{cls._counter:04d}"), "name": kwargs.get("name", f"test-host-{cls._counter}.local"), "ip_address": kwargs.get("ip_address", f"192.168.1.{cls._counter}"), "ansible_group": kwargs.get("ansible_group", "env_test"), "status": kwargs.get("status", "unknown"), "reachable": kwargs.get("reachable", False), "last_seen": kwargs.get("last_seen"), } @classmethod async def create(cls, session: AsyncSession, **kwargs) -> "Host": from app.crud.host import HostRepository data = cls.build(**kwargs) repo = HostRepository(session) host = await repo.create(**data) await session.commit() return host class TaskFactory: """Factory for creating test tasks.""" _counter = 0 @classmethod def build(cls, **kwargs) -> dict: cls._counter += 1 return { "id": kwargs.get("id", f"task-{cls._counter:04d}"), "action": kwargs.get("action", "health-check"), "target": kwargs.get("target", "all"), "playbook": kwargs.get("playbook", "health-check.yml"), "status": kwargs.get("status", "pending"), } @classmethod async def create(cls, session: AsyncSession, **kwargs) -> "Task": from app.crud.task import TaskRepository data = cls.build(**kwargs) repo = TaskRepository(session) task = await repo.create(**data) await session.commit() return task class UserFactory: """Factory for creating test users.""" _counter = 0 @classmethod def build(cls, **kwargs) -> dict: cls._counter += 1 from app.services.auth_service import hash_password return { "username": kwargs.get("username", f"testuser{cls._counter}"), "hashed_password": kwargs.get("hashed_password", hash_password("testpassword123")), "email": kwargs.get("email", f"test{cls._counter}@example.com"), "display_name": kwargs.get("display_name", f"Test User {cls._counter}"), "role": kwargs.get("role", "admin"), "is_active": kwargs.get("is_active", True), } @classmethod async def create(cls, session: AsyncSession, **kwargs) -> "User": from app.crud.user import UserRepository data = cls.build(**kwargs) repo = UserRepository(session) user = await repo.create(**data) await session.commit() return user class ScheduleFactory: """Factory for creating test schedules.""" _counter = 0 @classmethod def build(cls, **kwargs) -> dict: cls._counter += 1 return { "id": kwargs.get("id", f"schedule-{cls._counter:04d}"), "name": kwargs.get("name", f"Test Schedule {cls._counter}"), "playbook": kwargs.get("playbook", "health-check.yml"), "target": kwargs.get("target", "all"), "schedule_type": kwargs.get("schedule_type", "recurring"), "recurrence_type": kwargs.get("recurrence_type", "daily"), "recurrence_time": kwargs.get("recurrence_time", "02:00"), "enabled": kwargs.get("enabled", True), } @classmethod async def create(cls, session: AsyncSession, **kwargs) -> "Schedule": from app.crud.schedule import ScheduleRepository data = cls.build(**kwargs) repo = ScheduleRepository(session) schedule = await repo.create(**data) await session.commit() return schedule @pytest.fixture def host_factory(): """Provide HostFactory.""" HostFactory._counter = 0 return HostFactory @pytest.fixture def task_factory(): """Provide TaskFactory.""" TaskFactory._counter = 0 return TaskFactory @pytest.fixture def user_factory(): """Provide UserFactory.""" UserFactory._counter = 0 return UserFactory @pytest.fixture def schedule_factory(): """Provide ScheduleFactory.""" ScheduleFactory._counter = 0 return ScheduleFactory # ============================================================================ # UTILITY FIXTURES # ============================================================================ @pytest.fixture def api_headers(): """Provide standard API headers with auth.""" return { "X-API-Key": "test-api-key-12345", "Content-Type": "application/json", } @pytest.fixture def tmp_ansible_dir(tmp_path): """Create temporary Ansible directory structure.""" ansible_dir = tmp_path / "ansible" playbooks_dir = ansible_dir / "playbooks" inventory_dir = ansible_dir / "inventory" playbooks_dir.mkdir(parents=True) inventory_dir.mkdir(parents=True) # Create sample playbook (playbooks_dir / "health-check.yml").write_text(""" - name: Health Check hosts: all tasks: - name: Ping ping: """) # Create sample inventory (inventory_dir / "hosts.yml").write_text(""" all: children: env_test: hosts: test-host-1: ansible_host: 192.168.1.10 """) return ansible_dir