""" Tests for Models Client (Backend Integration) Test suite for the models client with fallback functionality. """ import pytest import asyncio from unittest.mock import Mock, AsyncMock, patch import httpx import sys import os # Add backend path for testing backend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "..", "cidadao.ai-backend") sys.path.insert(0, backend_path) from src.tools.models_client import ModelsClient, ModelAPIStatus class TestModelsClient: """Test suite for ModelsClient.""" @pytest.fixture def client(self): """Create models client instance.""" return ModelsClient( base_url="http://localhost:8001", timeout=5.0, enable_fallback=True ) @pytest.fixture def sample_contracts(self): """Sample contract data.""" return [ { "id": "CT001", "description": "Test contract", "value": 100000.0, "supplier": "Company A", "date": "2024-01-01", "organ": "Ministry X" } ] def test_client_initialization(self): """Test client initialization with different configs.""" # Default initialization client = ModelsClient() assert client.base_url == "http://localhost:8001" assert client.timeout == 30.0 assert client.enable_fallback is True assert client.status == ModelAPIStatus.ONLINE # Custom initialization client = ModelsClient( base_url="http://models:8080", timeout=10.0, enable_fallback=False ) assert client.base_url == "http://models:8080" assert client.timeout == 10.0 assert client.enable_fallback is False @pytest.mark.asyncio async def test_health_check_success(self, client): """Test successful health check.""" # Mock successful response with patch.object(client.client, 'get') as mock_get: mock_response = Mock() mock_response.json.return_value = { "status": "healthy", "models_loaded": True } mock_response.raise_for_status = Mock() mock_get.return_value = mock_response result = await client.health_check() assert result["status"] == "healthy" assert client.status == ModelAPIStatus.ONLINE assert client._failure_count == 0 @pytest.mark.asyncio async def test_health_check_failure(self, client): """Test health check failure handling.""" # Mock failed response with patch.object(client.client, 'get') as mock_get: mock_get.side_effect = httpx.RequestError("Connection failed") result = await client.health_check() assert result["status"] == "unhealthy" assert "error" in result assert result["fallback_available"] is True assert client._failure_count == 1 @pytest.mark.asyncio async def test_detect_anomalies_api_success(self, client, sample_contracts): """Test successful anomaly detection via API.""" expected_response = { "anomalies": [], "total_analyzed": 1, "anomalies_found": 0, "confidence_score": 0.95, "model_version": "1.0.0" } with patch.object(client.client, 'post') as mock_post: mock_response = Mock() mock_response.json.return_value = expected_response mock_response.raise_for_status = Mock() mock_post.return_value = mock_response result = await client.detect_anomalies(sample_contracts) assert result == expected_response assert client.status == ModelAPIStatus.ONLINE # Verify request was made correctly mock_post.assert_called_once() call_args = mock_post.call_args assert call_args[0][0] == "/v1/detect-anomalies" assert "contracts" in call_args[1]["json"] @pytest.mark.asyncio async def test_detect_anomalies_with_fallback(self, client, sample_contracts): """Test anomaly detection with fallback to local ML.""" # Mock API failure with patch.object(client.client, 'post') as mock_post: mock_post.side_effect = httpx.RequestError("API unavailable") # Mock local ML with patch.object(client, '_local_anomaly_detection') as mock_local: mock_local.return_value = { "anomalies": [], "total_analyzed": 1, "anomalies_found": 0, "confidence_score": 0.85, "model_version": "local-1.0.0", "source": "local_fallback" } result = await client.detect_anomalies(sample_contracts) assert result["source"] == "local_fallback" assert result["confidence_score"] == 0.85 mock_local.assert_called_once_with(sample_contracts, 0.7) @pytest.mark.asyncio async def test_detect_anomalies_no_fallback_error(self, client, sample_contracts): """Test anomaly detection error when fallback is disabled.""" client.enable_fallback = False with patch.object(client.client, 'post') as mock_post: mock_post.side_effect = httpx.RequestError("API unavailable") with pytest.raises(httpx.RequestError): await client.detect_anomalies(sample_contracts) @pytest.mark.asyncio async def test_analyze_patterns_success(self, client): """Test successful pattern analysis.""" test_data = {"contracts": [{"value": 100000}]} expected_response = { "patterns": [{"type": "temporal"}], "pattern_count": 1, "confidence": 0.88, "insights": ["Pattern detected"] } with patch.object(client.client, 'post') as mock_post: mock_response = Mock() mock_response.json.return_value = expected_response mock_response.raise_for_status = Mock() mock_post.return_value = mock_response result = await client.analyze_patterns(test_data) assert result == expected_response assert client.status == ModelAPIStatus.ONLINE @pytest.mark.asyncio async def test_analyze_spectral_success(self, client): """Test successful spectral analysis.""" time_series = [100, 200, 150, 300, 250] expected_response = { "frequencies": [0.1, 0.5], "amplitudes": [10.0, 20.0], "dominant_frequency": 0.5, "periodic_patterns": [] } with patch.object(client.client, 'post') as mock_post: mock_response = Mock() mock_response.json.return_value = expected_response mock_response.raise_for_status = Mock() mock_post.return_value = mock_response result = await client.analyze_spectral(time_series) assert result == expected_response assert result["dominant_frequency"] == 0.5 def test_circuit_breaker_functionality(self, client): """Test circuit breaker marks API as offline after failures.""" # Initial state assert client.status == ModelAPIStatus.ONLINE assert client._failure_count == 0 # First failure client._handle_failure() assert client.status == ModelAPIStatus.DEGRADED assert client._failure_count == 1 # Second failure client._handle_failure() assert client.status == ModelAPIStatus.DEGRADED assert client._failure_count == 2 # Third failure - circuit opens client._handle_failure() assert client.status == ModelAPIStatus.OFFLINE assert client._failure_count == 3 # Reset on success client._reset_failure_count() assert client.status == ModelAPIStatus.ONLINE assert client._failure_count == 0 @pytest.mark.asyncio async def test_context_manager(self): """Test client as async context manager.""" async with ModelsClient() as client: assert isinstance(client, ModelsClient) assert client.status == ModelAPIStatus.ONLINE # Client should be closed after context with pytest.raises(RuntimeError): await client.health_check() @pytest.mark.asyncio async def test_local_fallback_caching(self, client, sample_contracts): """Test local models are cached for performance.""" # Force use of local fallback client.status = ModelAPIStatus.OFFLINE # First call - creates model result1 = await client._local_anomaly_detection(sample_contracts, 0.7) assert "anomaly_detector" in client._local_models # Second call - reuses model initial_model = client._local_models["anomaly_detector"] result2 = await client._local_anomaly_detection(sample_contracts, 0.7) # Should be same model instance assert client._local_models["anomaly_detector"] is initial_model def test_singleton_client(self): """Test singleton client instance.""" from src.tools.models_client import get_models_client client1 = get_models_client() client2 = get_models_client() assert client1 is client2 # Same instance