"""Integration tests for provider selection based on availability, health status, priority.""" from unittest.mock import MagicMock, patch import pytest from src.core.providers.config_manager import ( ProviderConfigManager, ProviderSettings, ) from src.core.providers.failover import ProviderFailover from src.core.providers.health_monitor import ( ProviderHealthMetrics, ProviderHealthMonitor, ) class TestProviderSelectionByHealth: """Test provider selection based on health metrics.""" def test_best_provider_selected_by_success_rate(self): """Provider with highest success rate should be selected as best.""" monitor = ProviderHealthMonitor(failure_threshold=5) # Provider1: 80% success rate for i in range(10): monitor.record_request( "provider1", success=(i < 8), response_time_ms=100, error_message=None if i < 8 else "fail", ) # Provider2: 90% success rate for i in range(10): monitor.record_request( "provider2", success=(i < 9), response_time_ms=100, error_message=None if i < 9 else "fail", ) best = monitor.get_best_provider() assert best == "provider2" def test_unavailable_provider_not_selected(self): """Provider marked unavailable should not be selected as best.""" monitor = ProviderHealthMonitor(failure_threshold=3) # Make provider1 unavailable with consecutive failures for _ in range(5): monitor.record_request( "provider1", success=False, response_time_ms=500, error_message="Connection refused", ) # Provider2 is healthy monitor.record_request( "provider2", success=True, response_time_ms=100 ) best = monitor.get_best_provider() assert best == "provider2" available = monitor.get_available_providers() assert "provider1" not in available assert "provider2" in available def test_recovery_after_failures(self): """Provider should recover availability after successful request.""" monitor = ProviderHealthMonitor(failure_threshold=3) # Make provider fail for _ in range(4): monitor.record_request( "provider1", success=False, response_time_ms=200, error_message="fail" ) assert "provider1" not in monitor.get_available_providers() # Successful request should reset consecutive failures monitor.record_request( "provider1", success=True, response_time_ms=100 ) assert "provider1" in monitor.get_available_providers() def test_response_time_tiebreaker(self): """When success rates are equal, faster provider should be preferred.""" monitor = ProviderHealthMonitor() # Both have 100% success, but different response times monitor.record_request( "slow_provider", success=True, response_time_ms=500 ) monitor.record_request( "fast_provider", success=True, response_time_ms=50 ) best = monitor.get_best_provider() assert best == "fast_provider" class TestProviderSelectionWithConfig: """Test provider selection using configuration manager.""" def test_enabled_providers_only(self): """Only enabled providers should be available for selection.""" config = ProviderConfigManager() config.set_provider_settings( "p1", ProviderSettings(name="p1", enabled=True, priority=1) ) config.set_provider_settings( "p2", ProviderSettings(name="p2", enabled=False, priority=0) ) config.set_provider_settings( "p3", ProviderSettings(name="p3", enabled=True, priority=2) ) enabled = config.get_enabled_providers() assert "p1" in enabled assert "p2" not in enabled assert "p3" in enabled def test_priority_ordering(self): """Providers should be ordered by priority value.""" config = ProviderConfigManager() config.set_provider_settings( "low_priority", ProviderSettings(name="low_priority", priority=10), ) config.set_provider_settings( "high_priority", ProviderSettings(name="high_priority", priority=1), ) config.set_provider_settings( "mid_priority", ProviderSettings(name="mid_priority", priority=5), ) ordered = config.get_providers_by_priority() assert ordered == ["high_priority", "mid_priority", "low_priority"] def test_dynamic_priority_update(self): """Priority changes should immediately affect ordering.""" config = ProviderConfigManager() config.set_provider_settings( "p1", ProviderSettings(name="p1", priority=1) ) config.set_provider_settings( "p2", ProviderSettings(name="p2", priority=2) ) # Initially p1 is higher priority assert config.get_providers_by_priority()[0] == "p1" # Change p2 to higher priority config.set_provider_priority("p2", 0) assert config.get_providers_by_priority()[0] == "p2" class TestProviderSelectionWithFailover: """Test provider selection integration with failover system.""" def test_failover_respects_health_status(self): """Failover should prefer healthy providers.""" monitor = ProviderHealthMonitor(failure_threshold=2) # Mark p1 as unhealthy monitor.record_request("p1", False, 100, error_message="fail") monitor.record_request("p1", False, 100, error_message="fail") # p2 is healthy monitor.record_request("p2", True, 50) with patch( "src.core.providers.failover.get_health_monitor", return_value=monitor, ): failover = ProviderFailover( providers=["p1", "p2"], enable_health_monitoring=True, ) current = failover.get_current_provider() # Should prefer the healthy provider assert current == "p2" def test_failover_falls_back_to_round_robin(self): """Without health data, should use round-robin selection.""" failover = ProviderFailover( providers=["p1", "p2", "p3"], enable_health_monitoring=False, ) # Should cycle through providers first = failover.get_current_provider() assert first in ["p1", "p2", "p3"] class TestHealthMonitorMetrics: """Test health monitor metric collection scenarios.""" def test_metrics_tracking_accuracy(self): """Metrics should accurately reflect request history.""" monitor = ProviderHealthMonitor() # Record 7 successes and 3 failures for i in range(10): monitor.record_request( "test_provider", success=(i < 7), response_time_ms=100 + i * 10, bytes_transferred=1024 * (i + 1), error_message=None if i < 7 else f"error_{i}", ) metrics = monitor.get_provider_metrics("test_provider") assert metrics is not None assert metrics.total_requests == 10 assert metrics.successful_requests == 7 assert metrics.failed_requests == 3 assert metrics.success_rate == 70.0 assert metrics.total_bytes_downloaded == sum( 1024 * (i + 1) for i in range(10) ) def test_consecutive_failure_tracking(self): """Consecutive failures should be tracked accurately.""" monitor = ProviderHealthMonitor(failure_threshold=3) # 2 successes then 3 failures monitor.record_request("p1", True, 100) monitor.record_request("p1", True, 100) monitor.record_request("p1", False, 100, error_message="e1") monitor.record_request("p1", False, 100, error_message="e2") monitor.record_request("p1", False, 100, error_message="e3") metrics = monitor.get_provider_metrics("p1") assert metrics.consecutive_failures == 3 assert metrics.is_available is False def test_success_resets_consecutive_failures(self): """A success should reset the consecutive failure counter.""" monitor = ProviderHealthMonitor(failure_threshold=5) # 3 failures then 1 success monitor.record_request("p1", False, 100, error_message="e1") monitor.record_request("p1", False, 100, error_message="e2") monitor.record_request("p1", False, 100, error_message="e3") monitor.record_request("p1", True, 100) metrics = monitor.get_provider_metrics("p1") assert metrics.consecutive_failures == 0 assert metrics.is_available is True def test_health_summary(self): """Health summary should aggregate all provider metrics.""" monitor = ProviderHealthMonitor() monitor.record_request("p1", True, 100) monitor.record_request("p2", True, 200) monitor.record_request("p3", False, 300, error_message="err") summary = monitor.get_health_summary() assert summary["total_providers"] == 3 assert summary["available_providers"] >= 2 assert "average_success_rate" in summary assert "providers" in summary assert len(summary["providers"]) == 3 def test_reset_provider_metrics(self): """Resetting metrics should clear all data for a provider.""" monitor = ProviderHealthMonitor() monitor.record_request("p1", True, 100, bytes_transferred=1024) monitor.record_request("p1", False, 200, error_message="fail") result = monitor.reset_provider_metrics("p1") assert result is True metrics = monitor.get_provider_metrics("p1") assert metrics.total_requests == 0 assert metrics.successful_requests == 0 assert metrics.total_bytes_downloaded == 0 def test_reset_unknown_provider(self): """Resetting unknown provider should return False.""" monitor = ProviderHealthMonitor() assert monitor.reset_provider_metrics("unknown") is False def test_empty_summary(self): """Summary with no providers should return zeros.""" monitor = ProviderHealthMonitor() summary = monitor.get_health_summary() assert summary["total_providers"] == 0 assert summary["average_success_rate"] == 0.0 class TestMultiProviderHealthScenarios: """Test complex multi-provider health scenarios.""" def test_three_providers_degraded_service(self): """With 3 providers, partial failure should still select best.""" monitor = ProviderHealthMonitor(failure_threshold=3) # Provider A: fully down for _ in range(5): monitor.record_request("A", False, 500, error_message="down") # Provider B: degraded (50% success) for i in range(10): monitor.record_request( "B", success=(i % 2 == 0), response_time_ms=200, error_message=None if i % 2 == 0 else "intermittent" ) # Provider C: healthy (100% success) for _ in range(5): monitor.record_request("C", True, 100) best = monitor.get_best_provider() assert best == "C" available = monitor.get_available_providers() assert "A" not in available assert "B" in available assert "C" in available def test_all_providers_healthy(self): """When all healthy, fastest should be selected.""" monitor = ProviderHealthMonitor() monitor.record_request("slow", True, 500) monitor.record_request("medium", True, 200) monitor.record_request("fast", True, 50) best = monitor.get_best_provider() assert best == "fast" def test_no_providers_tracked(self): """With no tracked providers, best should be None.""" monitor = ProviderHealthMonitor() assert monitor.get_best_provider() is None assert monitor.get_available_providers() == []