""" Integration test fixtures with FastAPI TestClient setup. Provides database sessions, TestClient with dependency overrides, and authentication fixtures for API testing. """ import os import pytest import uuid import tempfile from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker # Set TESTING environment variable BEFORE any imports os.environ["TESTING"] = "1" # Global monkeypatch for SQLite compatibility: redirect JSONB to JSON try: from sqlalchemy.dialects.postgresql import JSONB import sqlalchemy.dialects.postgresql as postgresql import sqlalchemy.types as types postgresql.JSONB = types.JSON except ImportError: pass # Add parent directory to path for imports import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from main_api_app import app from core.auth import create_access_token from core.database import get_db, Base from core.models import User, AgentRegistry, AgentMemory, TokenUsage, AgentExecution, AgentStatus, ExecutionStatus from tests.factories.user_factory import AdminUserFactory @pytest.fixture(scope="function") def db_session(): """ Create a fresh in-memory database for each test. Simplified version that avoids sorted_tables to prevent NoReferencedTableError when running multiple tests in sequence. """ # Use file-based temp SQLite for tests fd, db_path = tempfile.mkstemp(suffix='.db') os.close(fd) engine = create_engine( f"sqlite:///{db_path}", connect_args={"check_same_thread": False}, echo=False ) # Store path for cleanup engine._test_db_path = db_path print(f"DEBUG: Tables in metadata: {list(Base.metadata.tables.keys())}") # Create tables print(f"DEBUG: Creating {len(Base.metadata.tables)} tables: {list(Base.metadata.tables.keys())}") Base.metadata.create_all(engine) # Create session TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False) # Global Patching: ensure all services use the same engine/session import core.database original_engine = core.database.engine original_sessionlocal = core.database.SessionLocal core.database.engine = engine core.database.SessionLocal = TestingSessionLocal session = TestingSessionLocal() yield session # Cleanup session.close() engine.dispose() # Delete temp database file if hasattr(engine, '_test_db_path'): try: os.unlink(engine._test_db_path) except Exception: pass @pytest.fixture(scope="function") def client(db_session: Session): """ Create TestClient with dependency override for test database. This fixture overrides the get_db dependency to use the test database session, ensuring all API requests use the test database with transaction rollback for isolation. Also bypasses authentication for integration tests. """ def _get_db(): try: yield db_session finally: pass # Transaction rolls back app.dependency_overrides[get_db] = _get_db # Override get_current_user to bypass auth - create lazy user factory def _mock_get_current_user(): """Mock get_current_user - creates or returns test user with admin role""" from tests.factories.user_factory import AdminUserFactory # Use unique email to avoid conflicts unique_id = str(uuid.uuid4())[:8] email = f"test_{unique_id}@integration.com" # Try to get existing user, handling case where tables don't exist yet try: user = db_session.query(User).filter(User.email == email).first() if user: return user except Exception as e: # Table doesn't exist or other error, will create user below pass # Create new admin user with all permissions user = AdminUserFactory(email=email, _session=db_session) db_session.commit() db_session.refresh(user) return user # Override get_current_user to bypass auth # Import from core.auth where it's actually defined try: from core.auth import get_current_user app.dependency_overrides[get_current_user] = _mock_get_current_user except ImportError: pass # Modify TrustedHostMiddleware to allow testserver # The middleware is stored as Middleware objects with cls and kwargs attributes for middleware in app.user_middleware: if hasattr(middleware, 'cls') and middleware.cls.__name__ == 'TrustedHostMiddleware': # Modify the allowed_hosts to include testserver middleware.kwargs['allowed_hosts'] = ['testserver', 'localhost', '127.0.0.1', '0.0.0.0', '*'] break # Create TestClient with proper headers test_client = TestClient(app, base_url="http://testserver") yield test_client app.dependency_overrides.clear() @pytest.fixture(scope="function") def client_no_auth(db_session: Session): """ Create TestClient WITHOUT bypassing authentication. This fixture provides a client that enforces authentication, useful for testing auth requirements and permissions. """ def _get_db(): try: yield db_session finally: pass # Transaction rolls back app.dependency_overrides[get_db] = _get_db # Do NOT override get_current_user - authentication is enforced # Modify TrustedHostMiddleware to allow testserver for middleware in app.user_middleware: if hasattr(middleware, 'cls') and middleware.cls.__name__ == 'TrustedHostMiddleware': middleware.kwargs['allowed_hosts'] = ['testserver', 'localhost', '127.0.0.1', '0.0.0.0', '*'] break # Create TestClient with proper headers test_client = TestClient(app, base_url="http://testserver") yield test_client app.dependency_overrides.clear() @pytest.fixture(scope="function") def auth_token(db_session: Session): """ Create valid JWT token for test user. Creates a test user in the database and returns a JWT token that can be used for authenticated requests. """ from tests.factories.user_factory import UserFactory unique_id = str(uuid.uuid4())[:8] user = UserFactory(email=f"auth_{unique_id}@integration.com", _session=db_session) db_session.add(user) db_session.commit() db_session.refresh(user) token = create_access_token(data={"sub": user.id}) return token @pytest.fixture(scope="function") def admin_token(db_session: Session): """ Create JWT token for admin user. Creates an admin user in the database and returns a JWT token with admin privileges for testing admin-only endpoints. """ from tests.factories.user_factory import AdminUserFactory unique_id = str(uuid.uuid4())[:8] admin = AdminUserFactory(email=f"admin_{unique_id}@integration.com", _session=db_session) db_session.add(admin) db_session.commit() db_session.refresh(admin) token = create_access_token(data={"sub": admin.id}) return token @pytest.fixture(scope="function") def test_user(db_session: Session): """ Create a test user in the database. Returns a User instance that can be used for testing user-related endpoints. """ from tests.factories.user_factory import UserFactory unique_id = str(uuid.uuid4())[:8] user = UserFactory(email=f"user_{unique_id}@integration.com", _session=db_session) db_session.add(user) db_session.commit() db_session.refresh(user) return user @pytest.fixture(scope="function") def auth_headers(auth_token: str): """ Create authentication headers for API requests. Returns a dictionary with Authorization header set to Bearer token. """ return {"Authorization": f"Bearer {auth_token}"} @pytest.fixture(scope="function") def admin_headers(admin_token: str): """ Create admin authentication headers for API requests. Returns a dictionary with Authorization header set to admin Bearer token. """ return {"Authorization": f"Bearer {admin_token}"} # ============================================================================ # E2E Test Fixtures for Agent Execution Workflow # ============================================================================ @pytest.fixture(scope="function") def e2e_db_session(db_session: Session): """ E2E database session with aggressive cleanup. Cleans up all E2E test data after each test to prevent cross-test contamination. """ yield db_session # Aggressive cleanup for E2E tests try: # Clean up in order of dependencies from sqlalchemy import text db_session.execute(text("DELETE FROM episode_segments WHERE 1=1")) db_session.execute(text("DELETE FROM agent_episodes WHERE agent_id LIKE 'test-agent%'")) db_session.execute(text("DELETE FROM agent_executions WHERE agent_id LIKE 'test-agent%'")) db_session.execute(text("DELETE FROM agent_registry WHERE id LIKE 'test-agent%'")) db_session.commit() except Exception as e: db_session.rollback() print(f"E2E cleanup error: {e}") @pytest.fixture(scope="function") def mock_llm_streaming(): """ Mock LLM streaming response for E2E tests. Returns an async generator that yields streaming chunks. """ async def stream_completion(*args, **kwargs): """Mock streaming completion with test response.""" chunks = [ "Test ", "response ", "chunk 1", "Test ", "response ", "chunk 2", "Test ", "response ", "chunk 3" ] for chunk in chunks: yield { "choices": [{ "delta": {"content": chunk}, "finish_reason": None }], "usage": None } # Final chunk with finish_reason yield { "choices": [{ "delta": {}, "finish_reason": "stop" }], "usage": { "prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30 } } return stream_completion @pytest.fixture(scope="function") def mock_llm_streaming_error(): """ Mock LLM streaming error for E2E error path tests. """ async def stream_completion_error(*args, **kwargs): """Mock streaming completion with error.""" yield { "choices": [{ "delta": {"content": "Initial chunk"}, "finish_reason": None }], "usage": None } # Simulate LLM API error raise Exception("LLM API error: rate limit exceeded") return stream_completion_error @pytest.fixture(scope="function") def e2e_client(client, e2e_db_session, mock_websocket): """ E2E test client with all necessary mocks. Combines TestClient with database session, WebSocket mocks, and authentication bypass for comprehensive E2E testing. """ yield client @pytest.fixture(scope="function") def execution_id(): """ Generate unique execution ID for E2E tests. """ import uuid return str(uuid.uuid4())