Bruno Charest ecefbc8611
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Clean up test files and debug artifacts, add node_modules to gitignore, export DashboardManager for testing, and enhance pytest configuration with comprehensive test markers and settings
2025-12-15 08:15:49 -05:00

412 lines
13 KiB
Python

"""
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