| """ |
| 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 |
|
|
| |
| os.environ["TESTING"] = "1" |
|
|
| |
| 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 |
|
|
| |
| 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. |
| """ |
| |
| 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 |
| ) |
|
|
| |
| engine._test_db_path = db_path |
|
|
| print(f"DEBUG: Tables in metadata: {list(Base.metadata.tables.keys())}") |
|
|
| |
| print(f"DEBUG: Creating {len(Base.metadata.tables)} tables: {list(Base.metadata.tables.keys())}") |
| Base.metadata.create_all(engine) |
|
|
| |
| TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False) |
| |
| |
| 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 |
|
|
| |
| session.close() |
| engine.dispose() |
| |
| 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 |
|
|
| app.dependency_overrides[get_db] = _get_db |
|
|
| |
| def _mock_get_current_user(): |
| """Mock get_current_user - creates or returns test user with admin role""" |
| from tests.factories.user_factory import AdminUserFactory |
|
|
| |
| unique_id = str(uuid.uuid4())[:8] |
| email = f"test_{unique_id}@integration.com" |
|
|
| |
| try: |
| user = db_session.query(User).filter(User.email == email).first() |
| if user: |
| return user |
| except Exception as e: |
| |
| pass |
|
|
| |
| user = AdminUserFactory(email=email, _session=db_session) |
| db_session.commit() |
| db_session.refresh(user) |
|
|
| return user |
|
|
| |
| |
| try: |
| from core.auth import get_current_user |
| app.dependency_overrides[get_current_user] = _mock_get_current_user |
| except ImportError: |
| pass |
|
|
| |
| |
| 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 |
|
|
| |
| 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 |
|
|
| app.dependency_overrides[get_db] = _get_db |
|
|
| |
|
|
| |
| 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 |
|
|
| |
| 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}"} |
|
|
|
|
| |
| |
| |
|
|
| @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 |
|
|
| |
| try: |
| |
| 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 |
| } |
| |
| 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 |
| } |
| |
| 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()) |
|
|