| """
|
| Enhanced API test fixtures for TestClient-based testing
|
|
|
| Provides reusable TestClient fixtures with proper isolation, authentication,
|
| and external service mocking for API route testing.
|
|
|
| This conftest is specific to API tests and does NOT duplicate fixtures from
|
| backend/tests/conftest.py (which provides db_session, test_agent_*, etc.)
|
| """
|
|
|
| import os
|
| import pytest
|
| from typing import Generator, Optional, Dict, Any
|
| from fastapi.testclient import TestClient
|
| from unittest.mock import MagicMock, AsyncMock, patch
|
| from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def api_test_client() -> Generator[TestClient, None, None]:
|
| """
|
| Create TestClient with proper isolation for API testing.
|
|
|
| Note: This is a placeholder fixture. Individual test files should
|
| create their own TestClient with specific routers to avoid
|
| SQLAlchemy metadata conflicts.
|
|
|
| Usage in test files:
|
| from fastapi import FastAPI
|
| from api.health_routes import router
|
|
|
| app = FastAPI()
|
| app.include_router(router)
|
|
|
| def test_something():
|
| client = TestClient(app)
|
| response = client.get("/health/live")
|
| """
|
|
|
| yield None
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def authenticated_client(
|
| api_test_client: TestClient,
|
| test_token: str
|
| ) -> Generator[TestClient, None, None]:
|
| """
|
| Create TestClient with pre-configured Authorization header.
|
|
|
| Usage:
|
| def test_protected_endpoint(authenticated_client):
|
| response = authenticated_client.get("/api/protected")
|
| assert response.status_code == 200
|
| """
|
|
|
| api_test_client.headers.update({
|
| "Authorization": f"Bearer {test_token}",
|
| "Content-Type": "application/json"
|
| })
|
| yield api_test_client
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def authenticated_admin_client(
|
| api_test_client: TestClient,
|
| admin_user: tuple
|
| ) -> Generator[TestClient, None, None]:
|
| """
|
| Create TestClient with admin Authorization header.
|
|
|
| Usage:
|
| def test_admin_endpoint(authenticated_admin_client):
|
| response = authenticated_admin_client.delete("/api/users/123")
|
| assert response.status_code == 200
|
| """
|
| user, token = admin_user
|
| api_test_client.headers.update({
|
| "Authorization": f"Bearer {token}",
|
| "Content-Type": "application/json",
|
| "X-User-Role": "admin"
|
| })
|
| yield api_test_client
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_llm_service() -> MagicMock:
|
| """
|
| Mock LLM service for agent chat endpoints.
|
|
|
| Usage:
|
| def test_agent_chat(mock_llm_service):
|
| mock_llm_service.complete.return_value = "Mock response"
|
| # Call endpoint that uses LLM service
|
| """
|
| mock = MagicMock()
|
|
|
|
|
| async def mock_stream(prompt: str, **kwargs):
|
| yield "Mock"
|
| yield " streaming"
|
| yield " response"
|
|
|
|
|
| mock.complete = MagicMock(return_value="Mock LLM response")
|
| mock.stream = mock_stream
|
| mock.validate_api_key = MagicMock(return_value=True)
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_playwright() -> MagicMock:
|
| """
|
| Mock Playwright for browser automation endpoints.
|
|
|
| Usage:
|
| def test_browser_screenshot(mock_playwright):
|
| mock_playwright.screenshot.return_value = "base64_image_data"
|
| # Call browser endpoint
|
| """
|
| mock = MagicMock()
|
|
|
|
|
| mock_browser = MagicMock()
|
| mock_page = MagicMock()
|
|
|
|
|
| mock_page.goto = MagicMock()
|
| mock_page.click = MagicMock()
|
| mock_page.fill = MagicMock()
|
| mock_page.screenshot = MagicMock(return_value=b"fake_screenshot_data")
|
| mock_page.evaluate = MagicMock(return_value="{}")
|
| mock_page.content = MagicMock(return_value="<html>Test</html>")
|
| mock_page.url = "https://example.com"
|
|
|
|
|
| mock_browser.new_page = MagicMock(return_value=mock_page)
|
| mock_browser.close = MagicMock()
|
|
|
|
|
| mock_browser.start = MagicMock(return_value=mock_browser)
|
| mock.stop = MagicMock()
|
|
|
| yield mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_storage_service() -> MagicMock:
|
| """
|
| Mock storage service for file operations.
|
|
|
| Usage:
|
| def test_file_upload(mock_storage_service):
|
| mock_storage_service.store.return_value = "https://storage.example.com/file.txt"
|
| # Call upload endpoint
|
| """
|
| mock = MagicMock()
|
|
|
|
|
| mock.store = MagicMock(return_value="https://mock-storage.example.com/file.txt")
|
| mock.retrieve = MagicMock(return_value=b"file contents")
|
| mock.delete = MagicMock(return_value=True)
|
| mock.exists = MagicMock(return_value=True)
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_websocket_manager() -> MagicMock:
|
| """
|
| Mock WebSocket manager for broadcast tests.
|
|
|
| Usage:
|
| def test_canvas_broadcast(mock_websocket_manager):
|
| mock_websocket_manager.broadcast.assert_called_once()
|
| """
|
| mock = MagicMock()
|
|
|
|
|
| mock.connect = MagicMock()
|
| mock.disconnect = MagicMock()
|
| mock.broadcast = MagicMock()
|
| mock.send_personal_message = MagicMock()
|
|
|
| return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def route_coverage() -> Dict[str, bool]:
|
| """
|
| Track which endpoints have been tested.
|
|
|
| Usage:
|
| def test_something(route_coverage):
|
| response = client.get("/api/endpoint")
|
| route_coverage["/api/endpoint"] = True
|
| """
|
| coverage = {}
|
|
|
| yield coverage
|
|
|
|
|
| if coverage:
|
| uncovered = [route for route, tested in coverage.items() if not tested]
|
| if uncovered:
|
| print(f"\n[WARNING] {len(uncovered)} routes not tested: {uncovered}")
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def api_test_headers(test_token: str) -> Dict[str, str]:
|
| """
|
| Generate test headers for API requests.
|
|
|
| Usage:
|
| def test_with_headers(api_test_headers):
|
| response = client.get("/api/test", headers=api_test_headers)
|
| """
|
| return {
|
| "Authorization": f"Bearer {test_token}",
|
| "Content-Type": "application/json",
|
| "Accept": "application/json"
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_device_permissions() -> MagicMock:
|
| """
|
| Mock device permission checks for device capability endpoints.
|
|
|
| Usage:
|
| def test_camera_access(mock_device_permissions):
|
| mock_device_permissions.check_camera.return_value = True
|
| response = client.post("/api/device/camera/request")
|
| """
|
| mock = MagicMock()
|
|
|
|
|
| mock.check_camera = MagicMock(return_value=True)
|
| mock.check_screen_recording = MagicMock(return_value=True)
|
| mock.check_location = MagicMock(return_value=True)
|
| mock.check_notifications = MagicMock(return_value=True)
|
|
|
|
|
| mock.get_camera_stream = MagicMock(return_value=b"fake_camera_data")
|
| mock.get_screen_capture = MagicMock(return_value=b"fake_screen_data")
|
| mock.get_location = MagicMock(return_value={"lat": 37.7749, "lon": -122.4194})
|
| mock.send_notification = MagicMock(return_value=True)
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_email_service() -> MagicMock:
|
| """
|
| Mock email service for password reset and notification tests.
|
|
|
| Usage:
|
| def test_password_reset(mock_email_service):
|
| mock_email_service.send_password_reset.assert_called_once()
|
| """
|
| mock = MagicMock()
|
|
|
|
|
| mock.send_password_reset = MagicMock(return_value=True)
|
| mock.send_verification_email = MagicMock(return_value=True)
|
| mock.send_notification = MagicMock(return_value=True)
|
|
|
| return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_totp() -> MagicMock:
|
| """
|
| Mock pyotp.TOTP class for 2FA testing.
|
|
|
| Usage:
|
| def test_enable_2fa(mock_totp):
|
| mock_totp.verify.return_value = True
|
| # Call enable endpoint
|
| """
|
| mock = MagicMock()
|
|
|
|
|
| mock.verify = MagicMock(return_value=True)
|
|
|
|
|
| mock.provisioning_uri = MagicMock(
|
| return_value="otpauth://totp/Atom%20AI:user@example.com?secret=TEST_SECRET_32_CHARS&issuer=Atom+AI+(Upstream)"
|
| )
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_pyotp_random() -> MagicMock:
|
| """
|
| Mock pyotp.random_base32 for deterministic 2FA testing.
|
|
|
| Returns a deterministic 32-character secret for testing.
|
|
|
| Usage:
|
| def test_setup_2fa(mock_pyotp_random):
|
| mock_pyotp_random.return_value = "JBSWY3DPEHPK3PXP"
|
| # Call setup endpoint
|
| """
|
|
|
| return "JBSWY3DPEHPK3PXP"
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def user_with_2fa() -> MagicMock:
|
| """
|
| Create mock user with 2FA already enabled.
|
|
|
| Usage:
|
| def test_disable_2fa(user_with_2fa):
|
| assert user_with_2fa.two_factor_enabled is True
|
| """
|
| from unittest.mock import Mock
|
| from core.models import User
|
|
|
| user = Mock(spec=User)
|
| user.id = "user-2fa-enabled"
|
| user.email = "2fa-user@example.com"
|
| user.two_factor_enabled = True
|
| user.two_factor_secret = "JBSWY3DPEHPK3PXP"
|
| user.two_factor_backup_codes = ["BACKUP-1234-5678"]
|
|
|
| return user
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def user_without_2fa() -> MagicMock:
|
| """
|
| Create mock user without 2FA enabled.
|
|
|
| Usage:
|
| def test_setup_2fa(user_without_2fa):
|
| assert user_without_2fa.two_factor_enabled is False
|
| """
|
| from unittest.mock import Mock
|
| from core.models import User
|
|
|
| user = Mock(spec=User)
|
| user.id = "user-no-2fa"
|
| user.email = "regular@example.com"
|
| user.two_factor_enabled = False
|
| user.two_factor_secret = None
|
| user.two_factor_backup_codes = None
|
|
|
| return user
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_audit_log() -> MagicMock:
|
| """
|
| Mock audit_service.log_event for 2FA audit testing.
|
|
|
| Tracks calls for verification in tests.
|
|
|
| Usage:
|
| def test_enable_2fa_logs_audit(mock_audit_log):
|
| mock_audit_log.assert_called_once()
|
| call_kwargs = mock_audit_log.call_args.kwargs
|
| assert call_kwargs["action"] == "2fa_enabled"
|
| """
|
| mock = MagicMock()
|
| mock.return_value = None
|
|
|
| return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_daemon_manager() -> MagicMock:
|
| """
|
| Mock DaemonManager class for agent control routes testing.
|
|
|
| Usage:
|
| def test_agent_start(mock_daemon_manager):
|
| mock_daemon_manager.is_running.return_value = False
|
| mock_daemon_manager.start_daemon.return_value = 12345
|
| response = client.post("/api/agent/start")
|
| """
|
| mock = MagicMock()
|
|
|
|
|
| mock.is_running = MagicMock(return_value=False)
|
| mock.get_pid = MagicMock(return_value=12345)
|
| mock.start_daemon = MagicMock(return_value=12345)
|
| mock.stop_daemon = MagicMock(return_value=None)
|
| mock.get_status = MagicMock(return_value={
|
| "running": True,
|
| "pid": 12345,
|
| "uptime_seconds": 3600,
|
| "memory_mb": 256.5,
|
| "cpu_percent": 5.2,
|
| "status": "running"
|
| })
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def test_daemon_status() -> dict:
|
| """
|
| Provide typical daemon status dict for testing.
|
|
|
| Usage:
|
| def test_status_endpoint(test_daemon_status):
|
| mock_daemon_manager.get_status.return_value = test_daemon_status
|
| response = client.get("/api/agent/status")
|
| assert response.json()["status"]["running"] == True
|
| """
|
| return {
|
| "running": True,
|
| "pid": 12345,
|
| "uptime_seconds": 3600,
|
| "memory_mb": 256.5,
|
| "cpu_percent": 5.2,
|
| "status": "running"
|
| }
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_running_daemon(mock_daemon_manager: MagicMock) -> MagicMock:
|
| """
|
| DaemonManager mock configured for "already running" test scenarios.
|
|
|
| Usage:
|
| def test_start_when_running(mock_running_daemon):
|
| response = client.post("/api/agent/start")
|
| assert response.status_code == 400
|
| """
|
| mock_daemon_manager.is_running.return_value = True
|
| mock_daemon_manager.get_pid.return_value = 12345
|
| return mock_daemon_manager
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_stopped_daemon(mock_daemon_manager: MagicMock) -> MagicMock:
|
| """
|
| DaemonManager mock configured for "not running" test scenarios.
|
|
|
| Usage:
|
| def test_stop_when_not_running(mock_stopped_daemon):
|
| response = client.post("/api/agent/stop")
|
| assert response.status_code == 400
|
| """
|
| mock_daemon_manager.is_running.return_value = False
|
| mock_daemon_manager.get_pid.return_value = None
|
| return mock_daemon_manager
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def test_pid() -> int:
|
| """
|
| Provide test PID for daemon operations.
|
|
|
| Usage:
|
| def test_daemon_start(test_pid):
|
| mock_daemon_manager.start_daemon.return_value = test_pid
|
| assert mock_daemon_manager.start_daemon() == 12345
|
| """
|
| return 12345
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_daemon_class():
|
| """
|
| Mock DaemonManager class with static method patching for agent control routes.
|
|
|
| This fixture patches the DaemonManager class in agent_control_routes module,
|
| allowing tests to control daemon behavior.
|
|
|
| Usage:
|
| def test_agent_start(mock_daemon_class):
|
| mock_daemon_class.is_running.return_value = False
|
| mock_daemon_class.start_daemon.return_value = 12345
|
| response = client.post("/api/agent/start")
|
| assert response.status_code == 200
|
| """
|
| from unittest.mock import patch
|
|
|
| with patch('api.agent_control_routes.DaemonManager') as mock_class:
|
|
|
| mock_class.is_running.return_value = False
|
| mock_class.get_pid.return_value = 12345
|
| mock_class.start_daemon.return_value = 12345
|
| mock_class.stop_daemon.return_value = None
|
| mock_class.get_status.return_value = {
|
| "running": True,
|
| "pid": 12345,
|
| "uptime_seconds": 3600,
|
| "memory_mb": 256.5,
|
| "cpu_percent": 5.2,
|
| "status": "running"
|
| }
|
| yield mock_class
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_mobile_device() -> MagicMock:
|
| """
|
| Mock MobileDevice for authentication testing.
|
|
|
| Usage:
|
| def test_mobile_login(mock_mobile_device):
|
| mock_mobile_device.device_token = "test_token_123"
|
| # Call login endpoint
|
| """
|
| from unittest.mock import Mock
|
| from datetime import datetime
|
|
|
| mock = Mock()
|
| mock.id = "device-test-123"
|
| mock.device_token = "test_device_token_123"
|
| mock.platform = "ios"
|
| mock.user_id = "user-test-123"
|
| mock.status = "active"
|
| mock.notification_enabled = True
|
| mock.device_info = {"model": "iPhone 14", "os_version": "16.0"}
|
| mock.last_active = datetime.utcnow()
|
| mock.created_at = datetime.utcnow()
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def test_user_with_device(db_session: Session) -> tuple:
|
| """
|
| Create test User with associated MobileDevice.
|
|
|
| Returns:
|
| tuple: (User, MobileDevice) for testing
|
|
|
| Usage:
|
| def test_mobile_auth(test_user_with_device):
|
| user, device = test_user_with_device
|
| assert user.email == "test-mobile@example.com"
|
| """
|
| import uuid
|
| from core.models import User, MobileDevice
|
|
|
| user_id = str(uuid.uuid4())
|
| device_id = str(uuid.uuid4())
|
|
|
| user = User(
|
| id=user_id,
|
| email=f"test-mobile-{user_id}@example.com",
|
| password_hash="hashed_password",
|
| first_name="Test",
|
| last_name="Mobile",
|
| role="member",
|
| status="active"
|
| )
|
|
|
| device = MobileDevice(
|
| id=device_id,
|
| user_id=user_id,
|
| device_token=f"device_token_{device_id}",
|
| platform="ios",
|
| status="active",
|
| notification_enabled=True,
|
| last_active=datetime.utcnow(),
|
| created_at=datetime.utcnow(),
|
| device_info={"model": "iPhone 14", "os_version": "16.0"}
|
| )
|
|
|
| db_session.add(user)
|
| db_session.add(device)
|
| db_session.commit()
|
| db_session.refresh(user)
|
| db_session.refresh(device)
|
|
|
| return (user, device)
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_auth_service() -> MagicMock:
|
| """
|
| Mock authentication service functions for auth routes testing.
|
|
|
| Usage:
|
| def test_login_with_mock(mock_auth_service):
|
| mock_auth_service['authenticate_mobile_user'].return_value = {
|
| "user": {"id": "123"},
|
| "access_token": "token"
|
| }
|
| # Call login endpoint
|
| """
|
| from unittest.mock import MagicMock
|
|
|
| mock = MagicMock()
|
|
|
|
|
| mock_auth_result = {
|
| "access_token": "test_access_token",
|
| "refresh_token": "test_refresh_token",
|
| "expires_at": "2026-03-12T17:00:00Z",
|
| "token_type": "bearer",
|
| "user": {
|
| "id": "user-test-123",
|
| "email": "test@example.com"
|
| }
|
| }
|
| mock.authenticate_mobile_user = MagicMock(return_value=mock_auth_result)
|
|
|
|
|
| mock.create_mobile_token = MagicMock(return_value=mock_auth_result)
|
|
|
|
|
| mock.verify_biometric_signature = MagicMock(return_value=True)
|
|
|
|
|
| mock.get_mobile_device = MagicMock(return_value=mock_mobile_device())
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def biometric_test_data() -> dict:
|
| """
|
| Provide biometric authentication test data.
|
|
|
| Returns dict with fake keys for testing (no real crypto).
|
|
|
| Usage:
|
| def test_biometric_auth(biometric_test_data):
|
| data = biometric_test_data
|
| response = client.post("/api/auth/mobile/biometric/authenticate", json=data)
|
| """
|
| return {
|
| "public_key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA" + "A" * 100,
|
| "device_token": "biometric_device_token_123",
|
| "signature": "mock_signature_" + "B" * 200,
|
| "challenge": "test_challenge_" + "C" * 50
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def test_users_with_roles() -> dict:
|
| """
|
| Create mock User objects for all UserRole enum values.
|
|
|
| Returns dict mapping UserRole to User mock objects.
|
| Useful for parametrized permission tests across all roles.
|
|
|
| Usage:
|
| def test_permission_matrix(test_users_with_roles):
|
| guest_user = test_users_with_roles[UserRole.GUEST]
|
| admin_user = test_users_with_roles[UserRole.SUPER_ADMIN]
|
| # Test permissions for each role
|
| """
|
| from unittest.mock import Mock
|
| from core.models import UserRole
|
|
|
| users = {}
|
|
|
| for role in UserRole:
|
| user = Mock()
|
| user.id = f"user-{role.value}"
|
| user.email = f"{role.value}@example.com"
|
| user.role = role.value
|
| user.status = "active"
|
| users[role] = user
|
|
|
| return users
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_rbac_check() -> MagicMock:
|
| """
|
| Mock RBACService.check_permission for permission verification tests.
|
|
|
| Tracks permission checks and returns configurable True/False.
|
|
|
| Usage:
|
| def test_permission_enforcement(mock_rbac_check):
|
| mock_rbac_check.return_value = True
|
| # Test endpoint that checks permission
|
| mock_rbac_check.assert_called_once_with(user, Permission.AGENT_RUN)
|
| """
|
| from core.rbac_service import RBACService
|
|
|
| mock = MagicMock()
|
| mock.check_permission = MagicMock(return_value=True)
|
| mock.get_user_permissions = MagicMock(return_value=set())
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def all_permissions() -> list:
|
| """
|
| Return list of all Permission enum values.
|
|
|
| Useful for parametrized tests across all permissions.
|
|
|
| Usage:
|
| @pytest.mark.parametrize("permission", all_permissions())
|
| def test_all_permissions(permission):
|
| # Test each permission
|
| """
|
| from core.rbac_service import Permission
|
|
|
| return list(Permission)
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def role_permission_mapping() -> dict:
|
| """
|
| Return ROLE_PERMISSIONS mapping from RBACService.
|
|
|
| Maps UserRole to Set[Permission] for verifying permission inheritance.
|
|
|
| Usage:
|
| def test_permission_inheritance(role_permission_mapping):
|
| guest_perms = role_permission_mapping[UserRole.GUEST]
|
| assert Permission.AGENT_VIEW in guest_perms
|
| assert Permission.AGENT_RUN not in guest_perms
|
| """
|
| from core.rbac_service import ROLE_PERMISSIONS
|
|
|
| return ROLE_PERMISSIONS
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_message_analytics() -> AsyncMock:
|
| """
|
| Mock MessageAnalyticsEngine for analytics routes testing.
|
|
|
| Returns deterministic analytics data with configurable filters.
|
|
|
| Usage:
|
| def test_analytics_summary(mock_message_analytics):
|
| mock_message_analytics.get_summary.return_value = {
|
| "message_stats": {"total_messages": 100},
|
| "sentiment_distribution": {"positive": 40, "negative": 20, "neutral": 40}
|
| }
|
| response = client.get("/api/analytics/summary")
|
| """
|
| from unittest.mock import AsyncMock
|
|
|
| mock = AsyncMock()
|
|
|
|
|
| mock.get_summary = AsyncMock(return_value={
|
| "message_stats": {
|
| "total_messages": 100,
|
| "total_words": 5000,
|
| "with_attachments": 25,
|
| "with_mentions": 40,
|
| "with_urls": 30
|
| },
|
| "sentiment_distribution": {
|
| "positive": 40,
|
| "negative": 20,
|
| "neutral": 40
|
| },
|
| "response_times": {
|
| "avg_response_seconds": 3600,
|
| "median_response_seconds": 2400,
|
| "p95_response_seconds": 7200
|
| },
|
| "activity_peaks": {
|
| "peak_days": ["Monday", "Tuesday", "Wednesday"],
|
| "messages_per_day": {"2026-03-12": 50, "2026-03-11": 45}
|
| },
|
| "cross_platform": {
|
| "platforms": {"slack": 60, "teams": 30, "gmail": 10},
|
| "most_active_platform": "slack",
|
| "total_messages": 100
|
| }
|
| })
|
|
|
|
|
| mock.get_sentiment_analysis = AsyncMock(return_value={
|
| "sentiment_distribution": {"positive": 40, "negative": 20, "neutral": 40},
|
| "sentiment_trend": [{"timestamp": "2026-03-12T00:00:00Z", "positive": 0.4}],
|
| "most_positive_topics": ["product launch", "feature request"],
|
| "most_negative_topics": ["bug report", "performance issue"]
|
| })
|
|
|
|
|
| mock.get_response_time_metrics = AsyncMock(return_value={
|
| "avg_response_seconds": 3600,
|
| "median_response_seconds": 2400,
|
| "p95_response_seconds": 7200,
|
| "p99_response_seconds": 10800,
|
| "response_time_distribution": [
|
| {"range": "0-1h", "count": 20},
|
| {"range": "1-4h", "count": 30},
|
| {"range": "4-24h", "count": 40}
|
| ],
|
| "slowest_threads": [
|
| {"thread_id": "thread1", "response_seconds": 10800, "participants": ["user1", "user2"]},
|
| {"thread_id": "thread2", "response_seconds": 9600, "participants": ["user3"]}
|
| ],
|
| "fastest_threads": [
|
| {"thread_id": "thread3", "response_seconds": 60, "participants": ["user4", "user5"]}
|
| ]
|
| })
|
|
|
|
|
| mock.get_activity_metrics = AsyncMock(return_value={
|
| "messages_per_hour": {"9": 10, "10": 15, "11": 12},
|
| "messages_per_day": {"2026-03-12": 50, "2026-03-11": 45},
|
| "messages_per_channel": {"#general": 30, "#random": 20},
|
| "peak_hours": [10, 11, 14],
|
| "peak_days": ["Monday", "Tuesday", "Wednesday"],
|
| "activity_heatmap": [
|
| {"hour": 9, "day": "Monday", "count": 10},
|
| {"hour": 10, "day": "Monday", "count": 15}
|
| ]
|
| })
|
|
|
|
|
| mock.get_cross_platform_analytics = AsyncMock(return_value={
|
| "platforms": {
|
| "slack": {"message_count": 60, "sentiment": {"positive": 25, "negative": 10, "neutral": 25}, "avg_response_time": 3000},
|
| "teams": {"message_count": 30, "sentiment": {"positive": 12, "negative": 7, "neutral": 11}, "avg_response_time": 3600},
|
| "gmail": {"message_count": 10, "sentiment": {"positive": 3, "negative": 3, "neutral": 4}, "avg_response_time": 7200}
|
| },
|
| "most_active_platform": "slack",
|
| "platform_comparison": [
|
| {"platform": "slack", "message_count": 60, "percentage": 60},
|
| {"platform": "teams", "message_count": 30, "percentage": 30},
|
| {"platform": "gmail", "message_count": 10, "percentage": 10}
|
| ]
|
| })
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_correlation_engine() -> AsyncMock:
|
| """
|
| Mock CrossPlatformCorrelationEngine for correlation testing.
|
|
|
| Returns deterministic correlation data with linked conversations.
|
|
|
| Usage:
|
| def test_correlations(mock_correlation_engine):
|
| mock_correlation_engine.correlate_conversations.return_value = [
|
| MockLinkedConversation(conversation_id="conv1", platforms={"slack", "teams"})
|
| ]
|
| response = client.post("/api/analytics/correlations", json=messages)
|
| """
|
| from unittest.mock import AsyncMock, Mock
|
| from core.cross_platform_correlation import LinkedConversation, CorrelationStrength
|
|
|
|
|
| mock_linked_conv = Mock(spec=LinkedConversation)
|
| mock_linked_conv.conversation_id = "linked-conv-123"
|
| mock_linked_conv.platforms = {"slack", "teams"}
|
| mock_linked_conv.participants = {"user1@example.com", "user2@example.com"}
|
| mock_linked_conv.message_count = 15
|
| mock_linked_conv.correlation_strength = CorrelationStrength.STRONG
|
| mock_linked_conv.unified_messages = [
|
| {"id": "msg1", "platform": "slack", "content": "Test message", "sender": "user1", "timestamp": "2026-03-12T10:00:00Z", "_correlation_source": "slack"},
|
| {"id": "msg2", "platform": "teams", "content": "Related message", "sender": "user2", "timestamp": "2026-03-12T10:05:00Z", "_correlation_source": "teams"}
|
| ]
|
|
|
| mock = AsyncMock()
|
|
|
|
|
| mock.correlate_conversations = AsyncMock(return_value=[mock_linked_conv])
|
|
|
|
|
| mock.get_unified_timeline = AsyncMock(return_value=[
|
| {"id": "msg1", "platform": "slack", "content": "Test message", "sender": "user1", "timestamp": "2026-03-12T10:00:00Z", "_correlation_source": "slack"},
|
| {"id": "msg2", "platform": "teams", "content": "Related message", "sender": "user2", "timestamp": "2026-03-12T10:05:00Z", "_correlation_source": "teams"}
|
| ])
|
|
|
|
|
| mock.cross_platform_links = [
|
| {"source_conv": "slack-conv-1", "target_conv": "teams-conv-2", "strength": 0.85}
|
| ]
|
|
|
|
|
| mock.linked_conversations = [mock_linked_conv]
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_insights_engine() -> AsyncMock:
|
| """
|
| Mock PredictiveInsightsEngine for predictive insights testing.
|
|
|
| Returns deterministic predictions with confidence levels.
|
|
|
| Usage:
|
| def test_predict_response_time(mock_insights_engine):
|
| mock_prediction = Mock()
|
| mock_prediction.predicted_seconds = 3600
|
| mock_prediction.confidence.value = "high"
|
| mock_insights_engine.predict_response_time.return_value = mock_prediction
|
| response = client.get("/api/analytics/predictions/response-time")
|
| """
|
| from unittest.mock import AsyncMock, Mock
|
| from core.predictive_insights import ResponseTimePrediction, ChannelRecommendation, BottleneckAlert, UserPattern, RecommendationConfidence, UrgencyLevel
|
|
|
|
|
| mock_prediction = Mock(spec=ResponseTimePrediction)
|
| mock_prediction.user_id = "user123"
|
| mock_prediction.predicted_seconds = 3600
|
| mock_prediction.confidence = RecommendationConfidence.HIGH
|
| mock_prediction.factors = ["Time of day", "Historical patterns", "Platform activity"]
|
|
|
|
|
| mock_recommendation = Mock(spec=ChannelRecommendation)
|
| mock_recommendation.user_id = "user123"
|
| mock_recommendation.recommended_platform = "slack"
|
| mock_recommendation.reason = "User is most active on Slack during this time"
|
| mock_recommendation.confidence = RecommendationConfidence.HIGH
|
| mock_recommendation.expected_response_time = 1800
|
| mock_recommendation.alternatives = ["teams", "gmail"]
|
|
|
|
|
| mock_bottleneck = Mock(spec=BottleneckAlert)
|
| mock_bottleneck.severity = UrgencyLevel.MEDIUM
|
| mock_bottleneck.thread_id = "thread-123"
|
| mock_bottleneck.platform = "slack"
|
| mock_bottleneck.description = "No response for 24 hours"
|
| mock_bottleneck.affected_users = ["user1@example.com", "user2@example.com"]
|
| mock_bottleneck.wait_time_seconds = 86400
|
| mock_bottleneck.suggested_action = "Send follow-up message or escalate"
|
|
|
|
|
| mock_pattern = Mock(spec=UserPattern)
|
| mock_pattern.user_id = "user123"
|
| mock_pattern.most_active_platform = "slack"
|
| mock_pattern.most_active_hours = [9, 10, 11, 14, 15]
|
| mock_pattern.avg_response_time = 3600
|
| mock_pattern.response_probability_by_hour = {
|
| 9: 0.8, 10: 0.9, 11: 0.7, 14: 0.85, 15: 0.75
|
| }
|
| mock_pattern.preferred_message_types = ["general", "question", "update"]
|
|
|
| mock = AsyncMock()
|
|
|
|
|
| mock.predict_response_time = AsyncMock(return_value=mock_prediction)
|
|
|
|
|
| mock.recommend_channel = AsyncMock(return_value=mock_recommendation)
|
|
|
|
|
| mock.detect_bottlenecks = AsyncMock(return_value=[mock_bottleneck])
|
|
|
|
|
| mock.get_user_pattern = AsyncMock(return_value=mock_pattern)
|
|
|
|
|
| mock.get_insights_summary = AsyncMock(return_value={
|
| "users_analyzed": 50,
|
| "bottlenecks_detected": 5,
|
| "avg_response_time_all_users": 3600,
|
| "most_active_platform": "slack",
|
| "peak_hours": [10, 11, 14]
|
| })
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def analytics_routes_client(mock_message_analytics, mock_correlation_engine, mock_insights_engine) -> TestClient:
|
| """
|
| Create TestClient with analytics routes router.
|
|
|
| Uses per-file FastAPI app pattern to avoid SQLAlchemy metadata conflicts.
|
| All analytics engines are mocked for deterministic testing.
|
|
|
| Usage:
|
| def test_analytics_summary(analytics_routes_client):
|
| response = analytics_routes_client.get("/api/analytics/summary")
|
| assert response.status_code == 200
|
| """
|
| from fastapi import FastAPI
|
| from api.analytics_dashboard_routes import router
|
| from unittest.mock import patch
|
|
|
| app = FastAPI()
|
| app.include_router(router)
|
|
|
|
|
| with patch('api.analytics_dashboard_routes.get_message_analytics_engine', return_value=mock_message_analytics), \
|
| patch('api.analytics_dashboard_routes.get_cross_platform_correlation_engine', return_value=mock_correlation_engine), \
|
| patch('api.analytics_dashboard_routes.get_predictive_insights_engine', return_value=mock_insights_engine):
|
|
|
| client = TestClient(app)
|
| yield client
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_feedback_analytics() -> AsyncMock:
|
| """
|
| Mock FeedbackAnalytics service for feedback analytics routes testing.
|
|
|
| Provides deterministic mock data for all analytics methods:
|
| - get_feedback_statistics() returns summary with counts, ratios, ratings
|
| - get_top_performing_agents() returns list of agent dicts
|
| - get_most_corrected_agents() returns list of agent dicts
|
| - get_feedback_breakdown_by_type() returns breakdown dict
|
| - get_feedback_trends() returns list of daily trends
|
| - get_agent_feedback_summary() returns agent-specific summary
|
|
|
| Usage:
|
| def test_feedback_dashboard(mock_feedback_analytics):
|
| mock_feedback_analytics.get_feedback_statistics.return_value = {
|
| "total_feedback": 100,
|
| "positive_count": 75,
|
| "negative_count": 25,
|
| "average_rating": 4.2
|
| }
|
| response = client.get("/api/feedback/analytics")
|
| """
|
| from unittest.mock import AsyncMock
|
|
|
| mock = AsyncMock()
|
|
|
|
|
| mock.get_feedback_statistics = AsyncMock(return_value={
|
| "total_feedback": 100,
|
| "positive_count": 75,
|
| "negative_count": 25,
|
| "thumbs_up_count": 60,
|
| "thumbs_down_count": 15,
|
| "average_rating": 4.2,
|
| "rating_distribution": {1: 5, 2: 8, 3: 12, 4: 30, 5: 45}
|
| })
|
|
|
|
|
| mock.get_top_performing_agents = AsyncMock(return_value=[
|
| {
|
| "agent_id": "agent-sales-001",
|
| "agent_name": "Sales Assistant",
|
| "total_feedback": 50,
|
| "average_rating": 4.8,
|
| "positive_ratio": 0.92
|
| },
|
| {
|
| "agent_id": "agent-support-001",
|
| "agent_name": "Support Bot",
|
| "total_feedback": 35,
|
| "average_rating": 4.6,
|
| "positive_ratio": 0.88
|
| }
|
| ])
|
|
|
|
|
| mock.get_most_corrected_agents = AsyncMock(return_value=[
|
| {
|
| "agent_id": "agent-data-001",
|
| "agent_name": "Data Analyst",
|
| "total_corrections": 15,
|
| "total_feedback": 40,
|
| "correction_rate": 0.375
|
| },
|
| {
|
| "agent_id": "agent-finance-001",
|
| "agent_name": "Finance Helper",
|
| "total_corrections": 12,
|
| "total_feedback": 30,
|
| "correction_rate": 0.40
|
| }
|
| ])
|
|
|
|
|
| mock.get_feedback_breakdown_by_type = AsyncMock(return_value={
|
| "thumbs_up": 60,
|
| "thumbs_down": 15,
|
| "rating": 20,
|
| "correction": 5
|
| })
|
|
|
|
|
| mock.get_feedback_trends = AsyncMock(return_value=[
|
| {
|
| "date": "2026-03-01",
|
| "total_feedback": 10,
|
| "positive_count": 8,
|
| "negative_count": 2,
|
| "average_rating": 4.3
|
| },
|
| {
|
| "date": "2026-03-02",
|
| "total_feedback": 12,
|
| "positive_count": 10,
|
| "negative_count": 2,
|
| "average_rating": 4.5
|
| }
|
| ])
|
|
|
|
|
| mock.get_agent_feedback_summary = AsyncMock(return_value={
|
| "agent_id": "agent-sales-001",
|
| "agent_name": "Sales Assistant",
|
| "total_feedback": 50,
|
| "positive_count": 46,
|
| "negative_count": 4,
|
| "thumbs_up_count": 38,
|
| "thumbs_down_count": 4,
|
| "average_rating": 4.8,
|
| "rating_distribution": {1: 0, 2: 1, 3: 3, 4: 8, 5: 38},
|
| "feedback_types": {"thumbs_up": 38, "thumbs_down": 4, "rating": 8}
|
| })
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_agent_learning() -> AsyncMock:
|
| """
|
| Mock AgentLearningEnhanced service for learning signal testing.
|
|
|
| Provides deterministic mock data for learning signals:
|
| - get_learning_signals() returns learning signals dict with:
|
| - improvement_suggestions: list of strings
|
| - common_corrections: list of strings
|
| - performance_trends: dict
|
|
|
| Handles agent_id and days parameters.
|
|
|
| Usage:
|
| def test_agent_dashboard_learning(mock_agent_learning):
|
| mock_agent_learning.get_learning_signals.return_value = {
|
| "improvement_suggestions": ["Improve accuracy"],
|
| "common_corrections": ["Fix calculation"],
|
| "performance_trends": {"accuracy": 0.85}
|
| }
|
| response = client.get("/api/feedback/agent/agent-001/analytics")
|
| """
|
| from unittest.mock import AsyncMock
|
|
|
| mock = AsyncMock()
|
|
|
|
|
| mock.get_learning_signals = AsyncMock(return_value={
|
| "improvement_suggestions": [
|
| "Improve response accuracy for technical queries",
|
| "Add more context to product recommendations",
|
| "Reduce response time for customer inquiries"
|
| ],
|
| "common_corrections": [
|
| "Pricing calculation errors",
|
| "Product availability mismatches",
|
| "Shipping estimate inaccuracies"
|
| ],
|
| "performance_trends": {
|
| "accuracy": 0.85,
|
| "response_time_ms": 450,
|
| "satisfaction_score": 4.2,
|
| "trend": "improving"
|
| }
|
| })
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def feedback_analytics_client(mock_feedback_analytics: AsyncMock, mock_agent_learning: AsyncMock) -> TestClient:
|
| """
|
| Create TestClient with feedback analytics router.
|
|
|
| Uses per-file FastAPI app pattern to avoid SQLAlchemy conflicts.
|
| Includes feedback_analytics.py router with mocked services.
|
|
|
| Usage:
|
| def test_feedback_dashboard(feedback_analytics_client):
|
| response = feedback_analytics_client.get("/api/feedback/analytics")
|
| assert response.status_code == 200
|
| """
|
| from fastapi import FastAPI
|
| from unittest.mock import patch
|
|
|
| app = FastAPI()
|
|
|
|
|
| async def mock_get_db():
|
| from unittest.mock import MagicMock
|
| mock_db = MagicMock()
|
| return mock_db
|
|
|
|
|
|
|
| with patch('api.feedback_analytics.FeedbackAnalytics', return_value=mock_feedback_analytics):
|
| with patch('core.agent_learning_enhanced.AgentLearningEnhanced', return_value=mock_agent_learning):
|
| from api.feedback_analytics import router
|
| app.include_router(router, prefix="/api/feedback/analytics")
|
|
|
|
|
| app.dependency_overrides[lambda: None] = mock_get_db
|
|
|
| client = TestClient(app)
|
| yield client
|
|
|
|
|
| app.dependency_overrides.clear()
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_db_session_feedback() -> MagicMock:
|
| """
|
| Mock Session for database dependency injection in feedback analytics.
|
|
|
| Used for get_db dependency in feedback analytics routes.
|
| Returns mock database session for testing.
|
|
|
| Usage:
|
| def test_with_mock_db(mock_db_session_feedback):
|
| mock_db_session_feedback.query.return_value.first.return_value = mock_agent
|
| # Test endpoint that uses get_db dependency
|
| """
|
| from unittest.mock import MagicMock
|
| from sqlalchemy.orm import Session
|
|
|
| mock = MagicMock(spec=Session)
|
|
|
|
|
| mock_query = MagicMock()
|
| mock.query = MagicMock(return_value=mock_query)
|
| mock_query.filter = MagicMock(return_value=mock_query)
|
| mock_query.all = MagicMock(return_value=[])
|
| mock_query.first = MagicMock(return_value=None)
|
|
|
| return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_ab_testing_service() -> AsyncMock:
|
| """
|
| Mock ABTestingService for A/B testing routes testing.
|
|
|
| Provides deterministic test data for all service methods.
|
| Simulates success and error paths for comprehensive testing.
|
|
|
| Usage:
|
| def test_create_ab_test(mock_ab_testing_service):
|
| mock_ab_testing_service.create_test.return_value = {
|
| "test_id": "test-123",
|
| "status": "draft"
|
| }
|
| response = client.post("/api/ab-tests/create", json={})
|
| """
|
| from unittest.mock import AsyncMock
|
| from datetime import datetime
|
|
|
| mock = AsyncMock()
|
|
|
|
|
| def create_test_success(**kwargs):
|
| return {
|
| "test_id": "test-123",
|
| "name": kwargs.get("name", "Test A"),
|
| "status": "draft",
|
| "test_type": kwargs.get("test_type", "prompt"),
|
| "agent_id": kwargs.get("agent_id", "test-agent"),
|
| "variant_a": {
|
| "name": kwargs.get("variant_a_name", "Control"),
|
| "config": kwargs.get("variant_a_config", {"temperature": 0.7})
|
| },
|
| "variant_b": {
|
| "name": kwargs.get("variant_b_name", "Treatment"),
|
| "config": kwargs.get("variant_b_config", {"temperature": 0.9})
|
| },
|
| "primary_metric": kwargs.get("primary_metric", "satisfaction_rate"),
|
| "min_sample_size": kwargs.get("min_sample_size", 100),
|
| "traffic_percentage": kwargs.get("traffic_percentage", 0.5)
|
| }
|
|
|
| mock.create_test = MagicMock(side_effect=create_test_success)
|
|
|
|
|
| def start_test_success(test_id: str):
|
| return {
|
| "test_id": test_id,
|
| "name": "Test A",
|
| "status": "running",
|
| "started_at": datetime.utcnow().isoformat()
|
| }
|
|
|
| mock.start_test = MagicMock(side_effect=start_test_success)
|
|
|
|
|
| def complete_test_success(test_id: str):
|
| return {
|
| "test_id": test_id,
|
| "name": "Test A",
|
| "status": "completed",
|
| "completed_at": datetime.utcnow().isoformat(),
|
| "variant_a_metrics": {
|
| "count": 150,
|
| "success_count": 120,
|
| "success_rate": 0.80,
|
| "average_metric_value": 4.2
|
| },
|
| "variant_b_metrics": {
|
| "count": 150,
|
| "success_count": 135,
|
| "success_rate": 0.90,
|
| "average_metric_value": 4.7
|
| },
|
| "p_value": 0.02,
|
| "winner": "B",
|
| "min_sample_size_reached": True
|
| }
|
|
|
| mock.complete_test = MagicMock(side_effect=complete_test_success)
|
|
|
|
|
| def assign_variant_success(test_id: str, user_id: str, session_id: str = None):
|
|
|
| import hashlib
|
| hash_value = int(hashlib.sha256(f"{test_id}:{user_id}".encode()).hexdigest(), 16)
|
| hash_fraction = (hash_value % 10000) / 10000.0
|
| variant = "B" if hash_fraction < 0.5 else "A"
|
|
|
| return {
|
| "test_id": test_id,
|
| "user_id": user_id,
|
| "variant": variant,
|
| "variant_name": "Control" if variant == "A" else "Treatment",
|
| "config": {"temperature": 0.7} if variant == "A" else {"temperature": 0.9},
|
| "existing_assignment": False
|
| }
|
|
|
| mock.assign_variant = MagicMock(side_effect=assign_variant_success)
|
|
|
|
|
| def record_metric_success(test_id: str, user_id: str, **kwargs):
|
| return {
|
| "test_id": test_id,
|
| "user_id": user_id,
|
| "variant": "A",
|
| "success": kwargs.get("success"),
|
| "metric_value": kwargs.get("metric_value"),
|
| "recorded_at": datetime.utcnow().isoformat()
|
| }
|
|
|
| mock.record_metric = MagicMock(side_effect=record_metric_success)
|
|
|
|
|
| def get_test_results_success(test_id: str):
|
| return {
|
| "test_id": test_id,
|
| "name": "Test A",
|
| "status": "completed",
|
| "test_type": "prompt",
|
| "primary_metric": "satisfaction_rate",
|
| "variant_a": {
|
| "name": "Control",
|
| "participant_count": 150,
|
| "metrics": {
|
| "count": 150,
|
| "success_count": 120,
|
| "success_rate": 0.80,
|
| "average_metric_value": 4.2
|
| }
|
| },
|
| "variant_b": {
|
| "name": "Treatment",
|
| "participant_count": 150,
|
| "metrics": {
|
| "count": 150,
|
| "success_count": 135,
|
| "success_rate": 0.90,
|
| "average_metric_value": 4.7
|
| }
|
| },
|
| "winner": "B",
|
| "statistical_significance": 0.02,
|
| "started_at": datetime.utcnow().isoformat(),
|
| "completed_at": datetime.utcnow().isoformat()
|
| }
|
|
|
| mock.get_test_results = MagicMock(side_effect=get_test_results_success)
|
|
|
|
|
| def list_tests_success(agent_id: str = None, status: str = None, limit: int = 50):
|
| tests = [
|
| {
|
| "test_id": "test-123",
|
| "name": "Test A",
|
| "status": "running",
|
| "test_type": "prompt",
|
| "agent_id": "agent-1",
|
| "primary_metric": "satisfaction_rate",
|
| "winner": None,
|
| "created_at": datetime.utcnow().isoformat()
|
| },
|
| {
|
| "test_id": "test-456",
|
| "name": "Test B",
|
| "status": "completed",
|
| "test_type": "agent_config",
|
| "agent_id": "agent-2",
|
| "primary_metric": "success_rate",
|
| "winner": "B",
|
| "created_at": datetime.utcnow().isoformat()
|
| }
|
| ]
|
|
|
|
|
| if agent_id:
|
| tests = [t for t in tests if t["agent_id"] == agent_id]
|
|
|
|
|
| if status:
|
| tests = [t for t in tests if t["status"] == status]
|
|
|
|
|
| tests = tests[:limit]
|
|
|
| return {
|
| "total": len(tests),
|
| "tests": tests
|
| }
|
|
|
| mock.list_tests = MagicMock(side_effect=list_tests_success)
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def sample_test_request() -> dict:
|
| """
|
| Factory for CreateTestRequest with valid default values.
|
|
|
| Provides valid test configuration data for creating A/B tests.
|
| All fields have sensible defaults that can be overridden.
|
|
|
| Usage:
|
| def test_create_test(sample_test_request):
|
| data = sample_test_request.copy()
|
| data["name"] = "Custom Test"
|
| response = client.post("/api/ab-tests/create", json=data)
|
| """
|
| return {
|
| "name": "Test A",
|
| "test_type": "prompt",
|
| "agent_id": "test-agent",
|
| "variant_a_config": {"temperature": 0.7},
|
| "variant_b_config": {"temperature": 0.9},
|
| "primary_metric": "satisfaction_rate",
|
| "traffic_percentage": 0.5,
|
| "min_sample_size": 100,
|
| "confidence_level": 0.95
|
| }
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def ab_testing_client() -> TestClient:
|
| """
|
| TestClient with A/B testing router.
|
|
|
| Creates per-file FastAPI app with ab_testing router.
|
| Avoids SQLAlchemy metadata conflicts by not importing main app.
|
|
|
| Usage:
|
| def test_ab_endpoint(ab_testing_client):
|
| response = ab_testing_client.get("/api/ab-tests")
|
| assert response.status_code == 200
|
| """
|
| from fastapi import FastAPI
|
| from api.ab_testing import router
|
|
|
| app = FastAPI()
|
| app.include_router(router, prefix="/api/ab-tests")
|
|
|
| return TestClient(app)
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_db_session() -> Session:
|
| """
|
| Mock Session for database dependency injection.
|
|
|
| Used to override get_db dependency in API routes.
|
| Provides deterministic mock database for testing.
|
|
|
| Usage:
|
| def test_with_mock_db(mock_db_session, ab_testing_client):
|
| # Override get_db dependency
|
| def override_get_db():
|
| yield mock_db_session
|
|
|
| app.dependency_overrides[get_db] = override_get_db
|
| # Make requests...
|
| """
|
| from unittest.mock import MagicMock
|
| from sqlalchemy.orm import Session
|
|
|
| mock = MagicMock(spec=Session)
|
|
|
|
|
| mock.add = MagicMock()
|
| mock.commit = MagicMock()
|
| mock.rollback = MagicMock()
|
| mock.refresh = MagicMock()
|
| mock.query = MagicMock()
|
| mock.flush = MagicMock()
|
| mock.close = MagicMock()
|
|
|
| return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def mock_workflow_analytics() -> MagicMock:
|
| """
|
| AsyncMock for WorkflowAnalyticsEngine with deterministic return values.
|
|
|
| Provides complete mock for all workflow analytics operations including:
|
| - Performance metrics (executions, success rate, duration)
|
| - Workflow metadata (names, IDs, execution times)
|
| - Execution timeline data
|
| - Error breakdown
|
| - Alerts management
|
| - Real-time events
|
|
|
| Usage:
|
| def test_get_dashboard_kpis(mock_workflow_analytics):
|
| mock_workflow_analytics.get_performance_metrics.return_value = test_metrics
|
| response = client.get("/api/analytics/dashboard/kpis")
|
| assert response.status_code == 200
|
| """
|
| from datetime import datetime, timedelta
|
| from unittest.mock import AsyncMock
|
|
|
| mock = AsyncMock()
|
|
|
|
|
| from collections import namedtuple
|
| PerformanceMetrics = namedtuple('PerformanceMetrics', [
|
| 'total_executions', 'successful_executions', 'failed_executions',
|
| 'success_rate', 'average_duration_ms', 'median_duration_ms',
|
| 'p95_duration_ms', 'p99_duration_ms', 'error_rate', 'unique_users',
|
| 'executions_by_user', 'most_common_errors', 'average_step_duration'
|
| ])
|
|
|
| mock_metrics = PerformanceMetrics(
|
| total_executions=100,
|
| successful_executions=95,
|
| failed_executions=5,
|
| success_rate=95.0,
|
| average_duration_ms=1500.0,
|
| median_duration_ms=1200.0,
|
| p95_duration_ms=3000.0,
|
| p99_duration_ms=5000.0,
|
| error_rate=5.0,
|
| unique_users=10,
|
| executions_by_user={},
|
| most_common_errors=[],
|
| average_step_duration={}
|
| )
|
| mock.get_performance_metrics.return_value = mock_metrics
|
|
|
|
|
| mock.get_all_workflow_ids.return_value = [
|
| "workflow-001",
|
| "workflow-002",
|
| "workflow-003"
|
| ]
|
|
|
|
|
| def get_workflow_name(workflow_id: str) -> str:
|
| names = {
|
| "workflow-001": "Data Import Pipeline",
|
| "workflow-002": "Email Campaign",
|
| "workflow-003": "Report Generation"
|
| }
|
| return names.get(workflow_id, workflow_id)
|
|
|
| mock.get_workflow_name.side_effect = get_workflow_name
|
|
|
|
|
| mock.get_last_execution_time.return_value = datetime.now()
|
|
|
|
|
| from collections import namedtuple
|
| ExecutionTimelineData = namedtuple('ExecutionTimelineData', [
|
| 'timestamp', 'count', 'success_count', 'failure_count', 'average_duration_ms'
|
| ])
|
|
|
| mock_timeline_data = [
|
| ExecutionTimelineData(
|
| timestamp=datetime.now() - timedelta(hours=1),
|
| count=10,
|
| success_count=9,
|
| failure_count=1,
|
| average_duration_ms=1500.0
|
| )
|
| ]
|
| mock.get_execution_timeline.return_value = mock_timeline_data
|
|
|
|
|
| mock.get_error_breakdown.return_value = {
|
| "ValidationError": 15,
|
| "TimeoutError": 8,
|
| "ConnectionError": 5
|
| }
|
|
|
|
|
| from collections import namedtuple
|
| Alert = namedtuple('Alert', [
|
| 'alert_id', 'name', 'description', 'severity', 'metric_name',
|
| 'condition', 'threshold_value', 'workflow_id', 'enabled',
|
| 'created_at', 'notification_channels'
|
| ])
|
|
|
| mock_alerts = [
|
| Alert(
|
| alert_id="alert-001",
|
| name="High Error Rate",
|
| description="Error rate exceeds 5%",
|
| severity="high",
|
| metric_name="error_rate",
|
| condition="error_rate > 5",
|
| threshold_value=5.0,
|
| workflow_id="workflow-001",
|
| enabled=True,
|
| created_at=datetime.now(),
|
| notification_channels=[]
|
| )
|
| ]
|
| mock.get_all_alerts.return_value = mock_alerts
|
|
|
|
|
| from collections import namedtuple
|
| RealtimeExecutionEvent = namedtuple('RealtimeExecutionEvent', [
|
| 'event_id', 'workflow_id', 'workflow_name', 'execution_id',
|
| 'event_type', 'timestamp', 'status', 'duration_ms', 'user_id'
|
| ])
|
|
|
| mock_events = [
|
| RealtimeExecutionEvent(
|
| event_id="event-001",
|
| workflow_id="workflow-001",
|
| workflow_name="Data Import Pipeline",
|
| execution_id="exec-001",
|
| event_type="workflow.completed",
|
| timestamp=datetime.now(),
|
| status="completed",
|
| duration_ms=1500,
|
| user_id="user-001"
|
| )
|
| ]
|
| mock.get_recent_events.return_value = mock_events
|
|
|
|
|
| mock.create_alert.return_value = "alert-002"
|
|
|
|
|
| mock.update_alert.return_value = True
|
|
|
|
|
| mock.delete_alert.return_value = True
|
|
|
|
|
| mock.get_unique_workflow_count.return_value = 3
|
|
|
| return mock
|
|
|
|
|
| @pytest.fixture(scope="function")
|
| def analytics_dashboard_test_client() -> TestClient:
|
| """
|
| TestClient with analytics dashboard router included.
|
|
|
| Uses per-file FastAPI app pattern to avoid SQLAlchemy metadata conflicts.
|
| Includes both analytics_dashboard_routes.py and analytics_dashboard_endpoints.py.
|
|
|
| Usage:
|
| def test_analytics_endpoint(analytics_dashboard_test_client):
|
| response = analytics_dashboard_test_client.get("/api/analytics/dashboard/kpis")
|
| assert response.status_code == 200
|
| """
|
| from fastapi import FastAPI
|
| from api.analytics_dashboard_routes import router as message_analytics_router
|
| from api.analytics_dashboard_endpoints import router as dashboard_router
|
|
|
| app = FastAPI()
|
| app.include_router(message_analytics_router)
|
| app.include_router(dashboard_router)
|
|
|
| return TestClient(app)
|
|
|