Spaces:
Paused
Paused
| """ | |
| Pytest configuration and fixtures for backend tests. | |
| Provides comprehensive mocking and fixtures for all test scenarios. | |
| """ | |
| import os | |
| import sys | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from unittest.mock import MagicMock, patch | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| from sqlalchemy import create_engine | |
| from sqlalchemy.orm import sessionmaker | |
| # Set test environment variables BEFORE any application imports | |
| os.environ.setdefault("ENVIRONMENT", "test") | |
| os.environ.setdefault("SECRET_KEY", "test-secret-key-min-32-chars-long-for-security") | |
| os.environ.setdefault("JWT_SECRET_KEY", "test-jwt-secret-key-min-32-chars") | |
| os.environ.setdefault("ENCRYPTION_KEY", "6SayK9nR4HO8REkIp3FhAaax8hwvLhsngvVd5OZa9Qc=") | |
| os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") | |
| os.environ.setdefault("CORS_ALLOWED_ORIGINS", "http://localhost:3000,http://localhost:5173") | |
| os.environ["TESTING"] = "True" | |
| # Add backend directory to Python path | |
| backend_dir = Path(__file__).parent.parent | |
| sys.path.insert(0, str(backend_dir)) | |
| # Import these after sys.path is updated | |
| from app.modules.auth.service import get_current_user # noqa: E402 | |
| from core.database import get_db # noqa: E402 | |
| # ===== Database Fixtures ===== | |
| def engine(): | |
| """Create test database engine with in-memory SQLite.""" | |
| from sqlalchemy.pool import StaticPool | |
| engine = create_engine( | |
| "sqlite:///:memory:", | |
| connect_args={"check_same_thread": False}, | |
| poolclass=StaticPool, | |
| ) | |
| # Import all models to ensure they are registered with Base | |
| # Importing from core.models ensures all models are loaded | |
| from core.models.base import Base | |
| Base.metadata.create_all(bind=engine) | |
| return engine | |
| def session_local(engine): | |
| """Create test session factory.""" | |
| return sessionmaker(autocommit=False, autoflush=False, bind=engine) | |
| def db_session(session_local): | |
| """Provide a clean database session for each test.""" | |
| session = session_local() | |
| try: | |
| yield session | |
| finally: | |
| session.close() | |
| def db_session_with_data(session_local): | |
| """Provide a database session with test data.""" | |
| from sqlalchemy import text | |
| from core.models import Case, FraudAlert, Project, Team, User | |
| session = session_local() | |
| # Clean up any existing data from previous tests to avoid IntegrityErrors | |
| session.execute(text("DELETE FROM fraud_alerts")) | |
| session.execute(text("DELETE FROM case_notes")) | |
| session.execute(text("DELETE FROM case_activities")) | |
| session.execute(text("DELETE FROM cases")) | |
| session.execute(text("DELETE FROM projects")) | |
| session.execute(text("DELETE FROM teams")) | |
| session.execute(text("DELETE FROM users")) | |
| session.commit() | |
| # Create test user | |
| test_user = User( | |
| id="test-user-001", | |
| username="testuser", | |
| email="test@example.com", | |
| full_name="Test User", | |
| role="ANALYST", | |
| is_active=True, | |
| mfa_enabled=False, | |
| password_hash="$pbkdf2_sha256$600000$test", | |
| preferences='{}', # Add preferences field | |
| created_at=datetime.now(timezone.utc), | |
| ) | |
| session.add(test_user) | |
| session.flush() | |
| # Create default team | |
| default_team = Team( | |
| id="default-team-001", | |
| name="Default Team", | |
| description="Default team for testing", | |
| ) | |
| session.add(default_team) | |
| session.flush() | |
| # Create default project | |
| default_project = Project( | |
| id="default", | |
| name="Default Project", | |
| description="Default project for testing", | |
| created_by="test-user-001", | |
| ) | |
| session.add(default_project) | |
| session.flush() | |
| # Create test case | |
| test_case = Case( | |
| id="test-case-001", | |
| project_id="default", | |
| title="Test Fraud Case", | |
| description="Test case for unit testing", | |
| status="OPEN", | |
| priority="HIGH", | |
| risk_score=75.0, | |
| assignee_id="test-user-001", | |
| team_id="default-team-001", | |
| created_at=datetime.now(timezone.utc), | |
| ) | |
| session.add(test_case) | |
| # Create test alert | |
| test_alert = FraudAlert( | |
| id="test-alert-001", | |
| case_id="test-case-001", | |
| alert_type="suspicious_transaction", | |
| severity="HIGH", | |
| title="Suspicious Activity Detected", | |
| is_acknowledged=False, | |
| description="Test alert for unit testing", | |
| created_at=datetime.now(timezone.utc), | |
| ) | |
| session.add(test_alert) | |
| session.commit() | |
| try: | |
| yield session | |
| finally: | |
| session.close() | |
| # ===== Authentication Fixtures ===== | |
| def mock_current_user(): | |
| """Mock authenticated user for tests.""" | |
| return { | |
| "id": "test-user-001", | |
| "username": "testuser", | |
| "email": "test@example.com", | |
| "role": "analyst", | |
| "mfa_verified": True, | |
| } | |
| def mock_admin_user(): | |
| """Mock admin user for tests.""" | |
| return { | |
| "id": "admin-user-001", | |
| "username": "admin", | |
| "email": "admin@example.com", | |
| "role": "admin", | |
| "mfa_verified": True, | |
| } | |
| def auth_headers(mock_current_user): | |
| """Generate authentication headers.""" | |
| from app.services.infrastructure.auth_service import auth_service | |
| token = auth_service.create_access_token(mock_current_user) | |
| return {"Authorization": f"Bearer {token}"} | |
| def admin_auth_headers(mock_admin_user): | |
| """Generate admin authentication headers.""" | |
| from app.services.infrastructure.auth_service import auth_service | |
| token = auth_service.create_access_token(mock_admin_user) | |
| return {"Authorization": f"Bearer {token}"} | |
| # ===== Service Mocks ===== | |
| def mock_auth_service(): | |
| """Mock auth service for tests.""" | |
| with patch("app.services.infrastructure.auth_service.auth_service") as mock: | |
| mock.authenticate_user.return_value = None | |
| mock.get_current_user.return_value = None | |
| mock.create_access_token.return_value = "test-jwt-token" | |
| yield mock | |
| def mock_db_service(): | |
| """Mock database service for tests.""" | |
| mock = MagicMock() | |
| mock.get_user_by_username.return_value = None | |
| mock.get_user_by_email.return_value = None | |
| mock.create_user.return_value = None | |
| mock.get_cases.return_value = [] | |
| mock.get_case_by_id.return_value = None | |
| return mock | |
| def mock_cache_service(): | |
| """Mock cache service for tests.""" | |
| mock = MagicMock() | |
| mock.get.return_value = None | |
| mock.set.return_value = True | |
| mock.delete.return_value = True | |
| mock.get_stats.return_value = { | |
| "l1_cache": {"entries": 0, "max_entries": 500}, | |
| "l2_cache": {"entries": 0, "max_entries": 1500}, | |
| "metrics": {"hits": 0, "misses": 0, "hit_rate": 0.0}, | |
| } | |
| return mock | |
| # ===== Application Fixtures ===== | |
| def app(): | |
| """Create test application instance.""" | |
| from app.factory import create_app | |
| app_instance = create_app() | |
| return app_instance | |
| def client(app): | |
| """Create test client with exceptions enabled for debugging.""" | |
| # Ensure performance_monitor is mocked even for simple client | |
| with patch("app.services.infrastructure.performance_monitor.performance_monitor.get_baselines") as mock_baselines: | |
| mock_baselines.return_value = { | |
| "monitoring_active": True, | |
| "metrics_collected": 100, | |
| } | |
| with patch( | |
| "app.services.infrastructure.performance_monitor.performance_monitor.get_current_metrics" | |
| ) as mock_metrics: | |
| mock_metrics.return_value = {"cpu_percent": 10.0, "memory_percent": 20.0} | |
| with patch( | |
| "app.services.infrastructure.performance_monitor.performance_monitor.check_thresholds" | |
| ) as mock_alerts: | |
| mock_alerts.return_value = [] | |
| with patch("app.services.infrastructure.circuit_breaker.get_all_circuit_breakers") as mock_cb: | |
| mock_cb.return_value = {} | |
| return TestClient(app, raise_server_exceptions=True) | |
| def client_with_db(app, db_session_with_data): | |
| """Create test client with database data and dependency overrides.""" | |
| from core.models import User | |
| # 1. Override database session | |
| def override_get_db(): | |
| try: | |
| yield db_session_with_data | |
| finally: | |
| pass | |
| # 2. Override get_current_user to return our test user | |
| def override_get_current_user(): | |
| return db_session_with_data.query(User).filter(User.id == "test-user-001").first() | |
| app.dependency_overrides[get_db] = override_get_db | |
| app.dependency_overrides[get_current_user] = override_get_current_user | |
| # 3. Patch db_service to use test session for health checks and internal service calls | |
| with ( | |
| patch("app.services.infrastructure.storage.database_service.db_service.get_db") as mock_get_db, | |
| patch("app.services.infrastructure.storage.database_service.db_service.health_check") as mock_health, | |
| patch("app.services.infrastructure.cache_service.cache_manager.get_stats") as mock_cache_stats, | |
| patch("app.services.infrastructure.performance_monitor.performance_monitor.get_baselines") as mock_baselines, | |
| patch( | |
| "app.services.infrastructure.performance_monitor.performance_monitor.get_current_metrics" | |
| ) as mock_metrics, | |
| patch("app.services.infrastructure.performance_monitor.performance_monitor.check_thresholds") as mock_alerts, | |
| patch("app.services.infrastructure.circuit_breaker.get_all_circuit_breakers") as mock_cb, | |
| ): | |
| mock_get_db.return_value = db_session_with_data | |
| # Mock health check to return healthy | |
| mock_health.return_value = { | |
| "status": "healthy", | |
| "checks": {"connectivity": {"status": "healthy"}}, | |
| "response_time_ms": 1.0, | |
| } | |
| # Mock cache service health check | |
| mock_cache_stats.return_value = { | |
| "l1_cache": {"entries": 0, "max_entries": 500}, | |
| "l2_cache": {"entries": 0, "max_entries": 1500}, | |
| "metrics": {"hits": 0, "misses": 0, "hit_rate": 0.0}, | |
| "l3_cache": {"available": True}, | |
| } | |
| # Mock performance monitor | |
| mock_baselines.return_value = { | |
| "monitoring_active": True, | |
| "metrics_collected": 100, | |
| } | |
| mock_metrics.return_value = {"cpu_percent": 5.0, "memory_percent": 10.0} | |
| mock_alerts.return_value = [] | |
| # Mock circuit breakers | |
| mock_cb.return_value = { | |
| "database_connection": {"state": "closed"}, | |
| "api_responsiveness": {"state": "closed"}, | |
| } | |
| client = TestClient(app, raise_server_exceptions=True) | |
| yield client | |
| app.dependency_overrides.clear() | |
| # ===== Helper Fixtures ===== | |
| def sample_case_data(): | |
| """Sample case data for create/update tests.""" | |
| return { | |
| "title": "New Test Case", | |
| "description": "Description for test case", | |
| "priority": "high", | |
| "tags": ["test", "automation"], | |
| } | |
| def sample_transaction_data(): | |
| """Sample transaction data for fraud analysis tests.""" | |
| return { | |
| "transaction_id": "txn-test-001", | |
| "amount": 15000.00, | |
| "currency": "USD", | |
| "merchant": "Test Merchant", | |
| "timestamp": datetime.now(timezone.utc).isoformat(), | |
| "account_id": "acc-001", | |
| } | |
| def sample_user_registration(): | |
| """Sample user registration data.""" | |
| return { | |
| "username": "newuser", | |
| "email": "newuser@example.com", | |
| "password": "SecureP@ss123!", | |
| "full_name": "New User", | |
| } | |
| def bulk_case_data(): | |
| """Fixture for bulk case data.""" | |
| return [ | |
| { | |
| "title": f"Bulk Case {i}", | |
| "description": f"Description for bulk case {i}", | |
| "priority": "Medium", | |
| "tags": ["bulk", "test"], | |
| } | |
| for i in range(5) | |
| ] | |
| def complex_case_with_notes(db_session_with_data): | |
| """Fixture for a case with multiple notes and attachments.""" | |
| from core.models import CaseNote | |
| case_id = "test-case-001" | |
| notes = [ | |
| CaseNote( | |
| id=f"note-{i}", | |
| case_id=case_id, | |
| content=f"Complex note content {i}", | |
| user_id="test-user-001", | |
| is_internal=(i % 2 == 0), | |
| created_at=datetime.now(timezone.utc), | |
| ) | |
| for i in range(3) | |
| ] | |
| for note in notes: | |
| db_session_with_data.add(note) | |
| db_session_with_data.commit() | |
| return case_id | |
| # ===== Environment Fixtures ===== | |
| def reset_env(): | |
| """Reset environment between tests.""" | |
| yield | |
| def set_production_env(): | |
| """Set production environment variables.""" | |
| with patch.dict( | |
| os.environ, | |
| { | |
| "ENVIRONMENT": "production", | |
| "DEBUG": "false", | |
| }, | |
| ): | |
| yield | |
| # ===== Async Fixtures ===== | |
| async def async_client(): | |
| """Create async test client for async endpoints.""" | |
| from httpx import AsyncClient | |
| from app.factory import create_app | |
| app = create_app() | |
| async with AsyncClient(app=app, base_url="http://test") as client: | |
| yield client | |
| # ===== Export commonly used items ===== | |
| # Store reference to tracer provider for cleanup | |
| _tracer_provider = None | |
| def _setup_telemetry_cleanup(): | |
| """Setup OpenTelemetry span processor cleanup after tests.""" | |
| global _tracer_provider | |
| try: | |
| from opentelemetry.sdk.resources import Resource | |
| from opentelemetry.sdk.trace import TracerProvider | |
| from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor | |
| _tracer_provider = TracerProvider(resource=Resource.create({})) | |
| _tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) | |
| except ImportError: | |
| pass | |
| yield | |
| # Cleanup after all tests complete | |
| if _tracer_provider is not None: | |
| try: | |
| _tracer_provider.shutdown() | |
| except Exception: | |
| pass | |
| def disable_telemetry_for_tests(): | |
| """Disable OpenTelemetry tracing during tests to avoid span export errors.""" | |
| import os | |
| # Disable any OTEL exporters that might try to write to closed files | |
| os.environ["OTEL_TRACES_EXPORTER"] = "none" | |
| os.environ["OTEL_METRICS_EXPORTER"] = "none" | |
| os.environ["OTEL_LOGS_EXPORTER"] = "none" | |
| yield | |
| # Clean up after test | |
| os.environ.pop("OTEL_TRACES_EXPORTER", None) | |
| os.environ.pop("OTEL_METRICS_EXPORTER", None) | |
| os.environ.pop("OTEL_LOGS_EXPORTER", None) | |
| __all__ = [ | |
| "engine", | |
| "session_local", | |
| "db_session", | |
| "db_session_with_data", | |
| "mock_current_user", | |
| "mock_admin_user", | |
| "auth_headers", | |
| "admin_auth_headers", | |
| "mock_auth_service", | |
| "mock_db_service", | |
| "mock_cache_service", | |
| "app", | |
| "client", | |
| "client_with_db", | |
| "sample_case_data", | |
| "sample_transaction_data", | |
| "sample_user_registration", | |
| "async_client", | |
| ] | |