| """ |
| 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. |
| |
| Provides a TestClient with the main API app for OpenAPI and |
| schema validation tests. Uses per-fixture app creation to avoid |
| SQLAlchemy metadata conflicts. |
| |
| Usage in test files: |
| def test_openapi_schema(api_test_client): |
| response = api_test_client.get("/openapi.json") |
| assert response.status_code == 200 |
| """ |
| from fastapi import FastAPI |
| from api.agent_routes import router as agent_router |
| from api.canvas_routes import router as canvas_router |
| from api.health_routes import router as health_router |
|
|
| app = FastAPI(title="Atom API Test") |
| app.include_router(agent_router) |
| app.include_router(canvas_router) |
| app.include_router(health_router) |
|
|
| client = TestClient(app) |
| yield client |
|
|
|
|
| @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 = Mock(return_value=[mock_linked_conv]) |
|
|
| |
| mock.get_unified_timeline = Mock(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, CommunicationPattern, 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=CommunicationPattern) |
| 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 = Mock(return_value=mock_prediction) |
|
|
| |
| mock.recommend_channel = Mock(return_value=mock_recommendation) |
|
|
| |
| mock.detect_bottlenecks = Mock(return_value=[mock_bottleneck]) |
|
|
| |
| mock.get_user_pattern = Mock(return_value=mock_pattern) |
|
|
| |
| mock.get_insights_summary = Mock(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() -> MagicMock: |
| """ |
| 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 = MagicMock() |
|
|
| |
| mock.get_feedback_statistics = MagicMock(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 = MagicMock(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 = MagicMock(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 = MagicMock(return_value={ |
| "thumbs_up": 60, |
| "thumbs_down": 15, |
| "rating": 20, |
| "correction": 5 |
| }) |
|
|
| |
| mock.get_feedback_trends = MagicMock(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 = MagicMock(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() -> MagicMock: |
| """ |
| 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 = MagicMock() |
|
|
| |
| mock.get_learning_signals = MagicMock(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 = MagicMock() |
|
|
| |
| 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 core.workflow_analytics_engine import AlertSeverity |
| |
| MockSeverity = namedtuple('MockSeverity', ['value']) |
|
|
| 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=MockSeverity('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) |
|
|