""" Property-Based Tests for Health Monitoring Tests correctness properties for health check and monitoring functionality. Uses Hypothesis for property-based testing. """ import pytest from hypothesis import given, strategies as st, settings, HealthCheck from unittest.mock import Mock, patch, AsyncMock from fastapi.testclient import TestClient from sqlmodel import Session, text from src.main import app from src.config.database import engine # Test client client = TestClient(app) def unwrap_response_data(response): payload = response.json() return payload.get("data", payload) # Strategies for generating test data dependency_names = st.sampled_from(['database', 'redis', 'task_manager', 'model_registry', 'ai_services']) health_statuses = st.sampled_from(['healthy', 'unhealthy', 'degraded', 'disabled']) error_messages = st.text(min_size=1, max_size=100) @given( db_healthy=st.booleans(), redis_healthy=st.booleans(), task_manager_healthy=st.booleans() ) @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=20) def test_property_24_health_check_dependency_validation( db_healthy: bool, redis_healthy: bool, task_manager_healthy: bool ): """ Property 24: 健康检查依赖验证 对于任何不健康的依赖(数据库、缓存、外部服务), 健康检查端点应该返回相应的错误状态码和详细信息。 **Validates: Requirements 18.3** """ # Mock dependencies based on health status with patch('src.api.health.Session') as mock_session_class, \ patch('src.services.cache_service.get_cache_service') as mock_cache_service, \ patch('src.api.health.task_manager') as mock_task_manager: # Setup database mock mock_session = Mock() mock_session_class.return_value.__enter__.return_value = mock_session if db_healthy: mock_session.exec.return_value = None # Successful query else: mock_session.exec.side_effect = Exception("Database connection failed") # Setup Redis mock mock_cache = Mock() mock_cache._connected = redis_healthy if redis_healthy: mock_cache._redis = AsyncMock() mock_cache._redis.ping = AsyncMock(return_value=True) mock_cache._redis.info = AsyncMock(return_value={ 'redis_version': '7.0.0', 'connected_clients': 1, 'used_memory_human': '1M' }) else: mock_cache._redis = AsyncMock() mock_cache._redis.ping = AsyncMock(side_effect=Exception("Redis connection failed")) mock_cache_service.return_value = mock_cache # Setup task manager mock if task_manager_healthy: mock_task_manager.get_stats.return_value = { 'total_tasks': 0, 'completed_tasks': 0, 'failed_tasks': 0, 'queue_size': 0 } else: mock_task_manager.get_stats.side_effect = Exception("Task manager error") # Call the detailed health check endpoint response = client.get("/health/detailed") # Verify response structure assert response.status_code == 200 data = unwrap_response_data(response) # Verify response has required fields assert "status" in data assert "components" in data assert "timestamp" in data components = data["components"] # Property: Database health should be reflected correctly if "database" in components: if db_healthy: assert components["database"]["status"] == "healthy" assert "latency_ms" in components["database"] else: assert components["database"]["status"] == "unhealthy" assert "message" in components["database"] assert "failed" in components["database"]["message"].lower() # Property: Redis health should be reflected correctly if "redis" in components: if redis_healthy: assert components["redis"]["status"] in ["healthy", "disabled"] else: assert components["redis"]["status"] in ["unhealthy", "disabled"] # Property: Task manager health should be reflected correctly if "task_manager" in components: if task_manager_healthy: assert components["task_manager"]["status"] == "healthy" assert "stats" in components["task_manager"] else: assert components["task_manager"]["status"] == "unhealthy" assert "message" in components["task_manager"] # Property: Overall status should be unhealthy if any critical component is unhealthy if not db_healthy: assert data["status"] in ["unhealthy", "degraded"] if not task_manager_healthy: assert data["status"] in ["unhealthy", "degraded"] @given( db_ready=st.booleans(), redis_enabled=st.booleans(), redis_ready=st.booleans(), task_manager_ready=st.booleans() ) @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=20) def test_property_24_readiness_probe_dependency_validation( db_ready: bool, redis_enabled: bool, redis_ready: bool, task_manager_ready: bool ): """ Property 24: 就绪探针依赖验证 对于任何不就绪的依赖,就绪探针应该返回503状态码。 **Validates: Requirements 18.3** """ # Mock dependencies based on readiness status with patch('src.api.health.Session') as mock_session_class, \ patch('src.services.cache_service.get_cache_service') as mock_cache_service, \ patch('src.api.health.task_manager') as mock_task_manager, \ patch('src.config.settings.REDIS_ENABLED', redis_enabled): # Setup database mock mock_session = Mock() mock_session_class.return_value.__enter__.return_value = mock_session if db_ready: mock_session.exec.return_value = None else: mock_session.exec.side_effect = Exception("Database not ready") # Setup Redis mock mock_cache = Mock() mock_cache._connected = redis_ready if redis_ready: mock_cache._redis = AsyncMock() mock_cache._redis.ping = AsyncMock(return_value=True) else: mock_cache._redis = AsyncMock() mock_cache._redis.ping = AsyncMock(side_effect=Exception("Redis not ready")) mock_cache_service.return_value = mock_cache # Setup task manager mock if task_manager_ready: mock_task_manager.get_stats.return_value = { 'total_tasks': 0, 'completed_tasks': 0, 'failed_tasks': 0, 'queue_size': 0 } else: mock_task_manager.get_stats.side_effect = Exception("Task manager not ready") # Call the readiness probe endpoint response = client.get("/health/ready") # Property: Should return 200 if all critical dependencies are ready, 503 otherwise # Critical dependencies: database, task_manager, and redis (only if enabled) all_ready = db_ready and task_manager_ready and (not redis_enabled or redis_ready) if all_ready: assert response.status_code == 200 data = unwrap_response_data(response) assert data["status"] == "ready" assert "components" in data else: assert response.status_code == 503 data = response.json() details = data.get("details") or data.get("detail") or {} if isinstance(details, dict): assert details["status"] == "not ready" assert "components" in details @given( component_name=dependency_names, is_healthy=st.booleans(), error_msg=error_messages ) @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=15) def test_property_24_component_error_details( component_name: str, is_healthy: bool, error_msg: str ): """ Property 24: 组件错误详情 对于任何不健康的组件,健康检查应该提供详细的错误信息。 **Validates: Requirements 18.3** """ # Mock the specific component to be unhealthy with patch('src.api.health.Session') as mock_session_class, \ patch('src.services.cache_service.get_cache_service') as mock_cache_service, \ patch('src.api.health.task_manager') as mock_task_manager, \ patch('src.api.health.ModelRegistry') as mock_registry, \ patch('src.api.health.health_monitor') as mock_health_monitor: # Setup all mocks as healthy by default mock_session = Mock() mock_session_class.return_value.__enter__.return_value = mock_session mock_session.exec.return_value = None mock_cache = Mock() mock_cache._connected = True mock_cache._redis = AsyncMock() mock_cache._redis.ping = AsyncMock(return_value=True) mock_cache._redis.info = AsyncMock(return_value={ 'redis_version': '7.0.0', 'connected_clients': 1, 'used_memory_human': '1M' }) mock_cache_service.return_value = mock_cache mock_task_manager.get_stats.return_value = { 'total_tasks': 0, 'completed_tasks': 0, 'failed_tasks': 0, 'queue_size': 0 } mock_registry.list_models.return_value = {} mock_health_monitor.get_health_summary.return_value = { 'total': 0, 'healthy': 0, 'unhealthy': 0, 'degraded': 0 } # Make the specific component unhealthy if not is_healthy: if component_name == 'database': mock_session.exec.side_effect = Exception(error_msg) elif component_name == 'redis': mock_cache._redis.ping.side_effect = Exception(error_msg) elif component_name == 'task_manager': mock_task_manager.get_stats.side_effect = Exception(error_msg) elif component_name == 'model_registry': mock_registry.list_models.side_effect = Exception(error_msg) elif component_name == 'ai_services': mock_health_monitor.get_health_summary.side_effect = Exception(error_msg) # Call the detailed health check endpoint response = client.get("/health/detailed") assert response.status_code == 200 data = unwrap_response_data(response) # Property: Unhealthy components should have error details if not is_healthy and component_name in data["components"]: component = data["components"][component_name] # Should have status field assert "status" in component # Should have message field with error details if component["status"] in ["unhealthy", "degraded"]: assert "message" in component # Error message should contain some information assert len(component["message"]) > 0 @given( num_unhealthy=st.integers(min_value=0, max_value=3) ) @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=10) def test_property_24_overall_status_aggregation(num_unhealthy: int): """ Property 24: 整体状态聚合 整体健康状态应该正确反映所有组件的健康状态。 如果有任何组件不健康,整体状态应该是unhealthy或degraded。 **Validates: Requirements 18.3** """ with patch('src.api.health.Session') as mock_session_class, \ patch('src.services.cache_service.get_cache_service') as mock_cache_service, \ patch('src.api.health.task_manager') as mock_task_manager: # Setup mocks mock_session = Mock() mock_session_class.return_value.__enter__.return_value = mock_session mock_cache = Mock() mock_cache._connected = True mock_cache._redis = AsyncMock() mock_cache._redis.ping = AsyncMock(return_value=True) mock_cache._redis.info = AsyncMock(return_value={ 'redis_version': '7.0.0', 'connected_clients': 1, 'used_memory_human': '1M' }) mock_cache_service.return_value = mock_cache # Make num_unhealthy components fail components_to_fail = ['database', 'task_manager', 'redis'][:num_unhealthy] if 'database' in components_to_fail: mock_session.exec.side_effect = Exception("Database failed") else: mock_session.exec.return_value = None if 'task_manager' in components_to_fail: mock_task_manager.get_stats.side_effect = Exception("Task manager failed") else: mock_task_manager.get_stats.return_value = { 'total_tasks': 0, 'completed_tasks': 0, 'failed_tasks': 0, 'queue_size': 0 } if 'redis' in components_to_fail: mock_cache._redis.ping.side_effect = Exception("Redis failed") # Call the detailed health check endpoint response = client.get("/health/detailed") assert response.status_code == 200 data = unwrap_response_data(response) # Property: Overall status should reflect component health if num_unhealthy == 0: # All healthy - overall should be healthy assert data["status"] == "healthy" else: # Some unhealthy - overall should be unhealthy or degraded assert data["status"] in ["unhealthy", "degraded"] if __name__ == "__main__": pytest.main([__file__, "-v", "--tb=short"])