Spaces:
Sleeping
Sleeping
| """ | |
| Shared pytest fixtures for Evolution Todo backend tests. | |
| This module provides fixtures for: | |
| - Database sessions with transaction rollback (for test isolation) | |
| - FastAPI test client with authentication | |
| - Mock objects for external services (OpenAI, Groq) | |
| - Test data factories for models | |
| - Sample audio files for voice transcription tests | |
| All tests use these fixtures to ensure consistency and isolation. | |
| """ | |
| import pytest | |
| import os | |
| import tempfile | |
| import jwt | |
| from datetime import datetime, timedelta | |
| from sqlmodel import Session, SQLModel, create_engine, select | |
| from fastapi.testclient import TestClient | |
| from unittest.mock import Mock, AsyncMock, patch, MagicMock | |
| from typing import Generator, Dict, Any | |
| import io | |
| # Import app and models | |
| from main import app | |
| from models import User, Task, Conversation, Message | |
| from db import get_session | |
| from middleware.auth import JWT_SECRET, JWT_ALGORITHM | |
| # ============================================================================== | |
| # Database Fixtures | |
| # ============================================================================== | |
| def test_engine(): | |
| """ | |
| Create a test database engine (session-scoped for performance). | |
| Uses in-memory SQLite for fast, isolated tests. | |
| All tables are created once per test session. | |
| """ | |
| # Use in-memory SQLite for tests (fast and isolated) | |
| engine = create_engine( | |
| "sqlite:///:memory:", | |
| echo=False, # Set to True for SQL debugging | |
| connect_args={"check_same_thread": False} # Required for SQLite | |
| ) | |
| # Create all tables | |
| SQLModel.metadata.create_all(engine) | |
| yield engine | |
| # Cleanup | |
| engine.dispose() | |
| def db_session(test_engine) -> Generator[Session, None, None]: | |
| """ | |
| Provide a database session with automatic rollback for test isolation. | |
| Each test gets a fresh transaction that is rolled back after the test, | |
| ensuring tests don't interfere with each other. | |
| Usage: | |
| def test_something(db_session): | |
| task = Task(user_id="user1", title="Test") | |
| db_session.add(task) | |
| db_session.commit() | |
| # Changes are rolled back automatically after test | |
| """ | |
| connection = test_engine.connect() | |
| transaction = connection.begin() | |
| session = Session(bind=connection) | |
| yield session | |
| # Rollback transaction (ensures test isolation) | |
| session.close() | |
| transaction.rollback() | |
| connection.close() | |
| def override_get_session(db_session: Session): | |
| """ | |
| Override FastAPI's get_session dependency with test database session. | |
| This ensures all API endpoints use the test database instead of production. | |
| """ | |
| def _override_get_session(): | |
| yield db_session | |
| app.dependency_overrides[get_session] = _override_get_session | |
| yield | |
| app.dependency_overrides.clear() | |
| # ============================================================================== | |
| # Authentication Fixtures | |
| # ============================================================================== | |
| def jwt_secret() -> str: | |
| """Return JWT secret for token generation.""" | |
| return JWT_SECRET | |
| def test_user(db_session: Session) -> User: | |
| """ | |
| Create a test user in the database. | |
| Returns: | |
| User with id="test_user_123", email="test@example.com" | |
| """ | |
| user = User( | |
| id="test_user_123", | |
| email="test@example.com", | |
| name="Test User", | |
| password_hash="hashed_password_here", | |
| created_at=datetime.utcnow(), | |
| updated_at=datetime.utcnow() | |
| ) | |
| db_session.add(user) | |
| db_session.commit() | |
| db_session.refresh(user) | |
| return user | |
| def test_user_2(db_session: Session) -> User: | |
| """ | |
| Create a second test user for multi-user tests. | |
| Returns: | |
| User with id="test_user_456", email="test2@example.com" | |
| """ | |
| user = User( | |
| id="test_user_456", | |
| email="test2@example.com", | |
| name="Test User 2", | |
| password_hash="hashed_password_here", | |
| created_at=datetime.utcnow(), | |
| updated_at=datetime.utcnow() | |
| ) | |
| db_session.add(user) | |
| db_session.commit() | |
| db_session.refresh(user) | |
| return user | |
| def auth_token(test_user: User, jwt_secret: str) -> str: | |
| """ | |
| Generate a valid JWT token for test_user. | |
| Returns: | |
| JWT token string for Authorization header | |
| """ | |
| payload = { | |
| "user_id": test_user.id, | |
| "exp": datetime.utcnow() + timedelta(hours=1) | |
| } | |
| token = jwt.encode(payload, jwt_secret, algorithm=JWT_ALGORITHM) | |
| return token | |
| def auth_token_user_2(test_user_2: User, jwt_secret: str) -> str: | |
| """ | |
| Generate a valid JWT token for test_user_2. | |
| Returns: | |
| JWT token string for Authorization header | |
| """ | |
| payload = { | |
| "user_id": test_user_2.id, | |
| "exp": datetime.utcnow() + timedelta(hours=1) | |
| } | |
| token = jwt.encode(payload, jwt_secret, algorithm=JWT_ALGORITHM) | |
| return token | |
| def expired_token(test_user: User, jwt_secret: str) -> str: | |
| """ | |
| Generate an expired JWT token for testing authentication failures. | |
| Returns: | |
| Expired JWT token string | |
| """ | |
| payload = { | |
| "user_id": test_user.id, | |
| "exp": datetime.utcnow() - timedelta(hours=1) # Expired 1 hour ago | |
| } | |
| token = jwt.encode(payload, jwt_secret, algorithm=JWT_ALGORITHM) | |
| return token | |
| def auth_headers(auth_token: str) -> Dict[str, str]: | |
| """ | |
| Generate HTTP headers with valid authentication token. | |
| Usage: | |
| response = client.post("/api/user123/chat", headers=auth_headers, json={...}) | |
| Returns: | |
| Dict with Authorization header | |
| """ | |
| return { | |
| "Authorization": f"Bearer {auth_token}" | |
| } | |
| def auth_headers_user_2(auth_token_user_2: str) -> Dict[str, str]: | |
| """ | |
| Generate HTTP headers with valid authentication token for user 2. | |
| Returns: | |
| Dict with Authorization header | |
| """ | |
| return { | |
| "Authorization": f"Bearer {auth_token_user_2}" | |
| } | |
| # ============================================================================== | |
| # FastAPI Test Client Fixtures | |
| # ============================================================================== | |
| def client(override_get_session) -> TestClient: | |
| """ | |
| Provide a FastAPI TestClient for API endpoint testing. | |
| The client automatically uses the test database (via override_get_session). | |
| Usage: | |
| def test_endpoint(client, auth_headers): | |
| response = client.post("/api/user123/chat", headers=auth_headers, json={...}) | |
| assert response.status_code == 200 | |
| Returns: | |
| TestClient instance | |
| """ | |
| return TestClient(app) | |
| # ============================================================================== | |
| # Mock Fixtures for External Services | |
| # ============================================================================== | |
| def mock_openai_client(): | |
| """ | |
| Mock OpenAI client for AI agent tests. | |
| Prevents actual API calls during tests and provides controlled responses. | |
| Usage: | |
| with mock_openai_client as mock_client: | |
| # Configure mock responses | |
| mock_client.chat.completions.create.return_value = ... | |
| """ | |
| with patch("agent.client") as mock_client: | |
| # Configure default mock behavior | |
| mock_response = MagicMock() | |
| mock_response.choices = [MagicMock()] | |
| mock_response.choices[0].message.content = "Mocked AI response" | |
| mock_response.choices[0].message.tool_calls = None | |
| mock_client.chat.completions.create.return_value = mock_response | |
| yield mock_client | |
| def mock_whisper_transcription(): | |
| """ | |
| Mock OpenAI Whisper transcription for voice tests. | |
| Prevents actual API calls and provides controlled transcription responses. | |
| Usage: | |
| with mock_whisper_transcription as mock_whisper: | |
| mock_whisper.return_value.text = "Custom transcription" | |
| """ | |
| with patch("routes.voice.client") as mock_client: | |
| # Configure default transcription response | |
| mock_transcription = MagicMock() | |
| mock_transcription.text = "This is a test transcription" | |
| mock_transcription.language = "en" | |
| mock_client.audio.transcriptions.create.return_value = mock_transcription | |
| yield mock_client | |
| def mock_mcp_tools(): | |
| """ | |
| Mock MCP tools for agent tests. | |
| Prevents actual tool execution and provides controlled tool responses. | |
| """ | |
| with patch("agent.list_tools") as mock_list_tools, \ | |
| patch("agent.call_tool") as mock_call_tool: | |
| # Mock tool list | |
| mock_list_tools.return_value = [] | |
| # Mock tool execution | |
| from mcp.types import TextContent | |
| mock_call_tool.return_value = [ | |
| TextContent(type="text", text="Mocked tool result") | |
| ] | |
| yield { | |
| "list_tools": mock_list_tools, | |
| "call_tool": mock_call_tool | |
| } | |
| # ============================================================================== | |
| # Test Data Fixtures | |
| # ============================================================================== | |
| def sample_task(db_session: Session, test_user: User) -> Task: | |
| """ | |
| Create a sample task for testing. | |
| Returns: | |
| Task with title="Sample Task", user_id=test_user.id | |
| """ | |
| task = Task( | |
| user_id=test_user.id, | |
| title="Sample Task", | |
| description="This is a sample task for testing", | |
| completed=False, | |
| priority="medium", | |
| tags=["test", "sample"], | |
| created_at=datetime.utcnow(), | |
| updated_at=datetime.utcnow() | |
| ) | |
| db_session.add(task) | |
| db_session.commit() | |
| db_session.refresh(task) | |
| return task | |
| def sample_conversation(db_session: Session, test_user: User) -> Conversation: | |
| """ | |
| Create a sample conversation for testing. | |
| Returns: | |
| Conversation with user_id=test_user.id | |
| """ | |
| conversation = Conversation( | |
| user_id=test_user.id, | |
| created_at=datetime.utcnow(), | |
| updated_at=datetime.utcnow() | |
| ) | |
| db_session.add(conversation) | |
| db_session.commit() | |
| db_session.refresh(conversation) | |
| return conversation | |
| def sample_messages(db_session: Session, test_user: User, sample_conversation: Conversation) -> list[Message]: | |
| """ | |
| Create sample messages in a conversation. | |
| Returns: | |
| List of 3 messages (user -> assistant -> user) | |
| """ | |
| messages = [ | |
| Message( | |
| conversation_id=sample_conversation.id, | |
| user_id=test_user.id, | |
| role="user", | |
| content="Hello, create a task for me", | |
| created_at=datetime.utcnow() | |
| ), | |
| Message( | |
| conversation_id=sample_conversation.id, | |
| user_id=test_user.id, | |
| role="assistant", | |
| content="Sure! What task would you like to create?", | |
| tool_calls=[], | |
| created_at=datetime.utcnow() | |
| ), | |
| Message( | |
| conversation_id=sample_conversation.id, | |
| user_id=test_user.id, | |
| role="user", | |
| content="Buy groceries tomorrow", | |
| created_at=datetime.utcnow() | |
| ) | |
| ] | |
| for msg in messages: | |
| db_session.add(msg) | |
| db_session.commit() | |
| return messages | |
| def sample_audio_file() -> io.BytesIO: | |
| """ | |
| Create a sample audio file (binary data) for voice transcription tests. | |
| Returns: | |
| BytesIO object with fake audio data | |
| """ | |
| # Create fake audio data (in production, this would be actual audio bytes) | |
| audio_data = b"RIFF" + b"\x00" * 100 # Minimal WAV-like header + data | |
| return io.BytesIO(audio_data) | |
| # ============================================================================== | |
| # Task Factory Fixtures | |
| # ============================================================================== | |
| def task_factory(db_session: Session): | |
| """ | |
| Factory fixture for creating multiple tasks with custom attributes. | |
| Usage: | |
| def test_multiple_tasks(task_factory): | |
| task1 = task_factory(title="Task 1", priority="high") | |
| task2 = task_factory(title="Task 2", completed=True) | |
| Returns: | |
| Factory function that creates tasks | |
| """ | |
| def _create_task( | |
| user_id: str = "test_user_123", | |
| title: str = "Test Task", | |
| description: str = None, | |
| completed: bool = False, | |
| priority: str = "none", | |
| tags: list = None, | |
| due_date: datetime = None, | |
| recurrence_pattern: str = None | |
| ) -> Task: | |
| task = Task( | |
| user_id=user_id, | |
| title=title, | |
| description=description, | |
| completed=completed, | |
| priority=priority, | |
| tags=tags or [], | |
| due_date=due_date, | |
| recurrence_pattern=recurrence_pattern, | |
| created_at=datetime.utcnow(), | |
| updated_at=datetime.utcnow() | |
| ) | |
| db_session.add(task) | |
| db_session.commit() | |
| db_session.refresh(task) | |
| return task | |
| return _create_task | |
| def conversation_factory(db_session: Session): | |
| """ | |
| Factory fixture for creating multiple conversations. | |
| Usage: | |
| def test_multiple_conversations(conversation_factory): | |
| conv1 = conversation_factory(user_id="user1") | |
| conv2 = conversation_factory(user_id="user2") | |
| Returns: | |
| Factory function that creates conversations | |
| """ | |
| def _create_conversation(user_id: str = "test_user_123") -> Conversation: | |
| conversation = Conversation( | |
| user_id=user_id, | |
| created_at=datetime.utcnow(), | |
| updated_at=datetime.utcnow() | |
| ) | |
| db_session.add(conversation) | |
| db_session.commit() | |
| db_session.refresh(conversation) | |
| return conversation | |
| return _create_conversation | |