""" Integration tests for miscellaneous components. Tests configuration system integration, error handling pipelines, and modular architecture component interactions. """ import pytest import sys import os from unittest.mock import Mock import json import tempfile from pathlib import Path # Add source directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..')) @pytest.mark.integration class TestConfigurationIntegration: """Test configuration system integration.""" def test_config_loading_chain(self): """Test complete configuration loading chain.""" # Create temporary config files with tempfile.TemporaryDirectory() as temp_dir: # Create default config default_config = { "anime_directory": "/default/path", "log_level": "INFO", "provider_timeout": 30 } # Create user config that overrides some values user_config = { "anime_directory": "/user/path", "log_level": "DEBUG" } default_file = Path(temp_dir) / "default.json" user_file = Path(temp_dir) / "user.json" with open(default_file, 'w') as f: json.dump(default_config, f) with open(user_file, 'w') as f: json.dump(user_config, f) # Mock configuration loader def load_configuration(default_path, user_path): """Load configuration with precedence.""" config = {} # Load default config if os.path.exists(default_path): with open(default_path, 'r') as f: config.update(json.load(f)) # Load user config (overrides defaults) if os.path.exists(user_path): with open(user_path, 'r') as f: config.update(json.load(f)) return config # Test configuration loading config = load_configuration(str(default_file), str(user_file)) # Verify precedence assert config["anime_directory"] == "/user/path" # User override assert config["log_level"] == "DEBUG" # User override assert config["provider_timeout"] == 30 # Default value def test_config_validation_integration(self): """Test configuration validation integration.""" def validate_config(config): """Validate configuration values.""" errors = [] # Validate required fields required_fields = ["anime_directory", "log_level"] for field in required_fields: if field not in config: errors.append(f"Missing required field: {field}") # Validate specific values if "log_level" in config: valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "FATAL"] if config["log_level"] not in valid_levels: errors.append(f"Invalid log level: {config['log_level']}") if "provider_timeout" in config: if config["provider_timeout"] <= 0: errors.append("Provider timeout must be positive") return errors # Test valid configuration valid_config = { "anime_directory": "/valid/path", "log_level": "INFO", "provider_timeout": 30 } errors = validate_config(valid_config) assert len(errors) == 0 # Test invalid configuration invalid_config = { "log_level": "INVALID", "provider_timeout": -5 } errors = validate_config(invalid_config) assert len(errors) == 3 # Missing anime_directory, invalid log level, negative timeout assert "Missing required field: anime_directory" in errors assert "Invalid log level: INVALID" in errors assert "Provider timeout must be positive" in errors def test_config_change_propagation(self): """Test configuration change propagation to components.""" class ConfigurableComponent: def __init__(self, config_manager): self.config_manager = config_manager self.current_config = {} self.config_manager.add_observer(self.on_config_change) def on_config_change(self, key, old_value, new_value): self.current_config[key] = new_value # React to specific config changes if key == "log_level": self.update_log_level(new_value) elif key == "provider_timeout": self.update_timeout(new_value) def update_log_level(self, level): self.log_level_changed = level def update_timeout(self, timeout): self.timeout_changed = timeout # Mock config manager class ConfigManager: def __init__(self): self.config = {} self.observers = [] def add_observer(self, observer): self.observers.append(observer) def set(self, key, value): old_value = self.config.get(key) self.config[key] = value for observer in self.observers: observer(key, old_value, value) # Test configuration change propagation config_manager = ConfigManager() component = ConfigurableComponent(config_manager) # Change configuration config_manager.set("log_level", "DEBUG") config_manager.set("provider_timeout", 60) # Verify changes propagated assert component.current_config["log_level"] == "DEBUG" assert component.current_config["provider_timeout"] == 60 assert component.log_level_changed == "DEBUG" assert component.timeout_changed == 60 @pytest.mark.integration class TestErrorHandlingIntegration: """Test error handling system integration.""" def test_error_propagation_chain(self): """Test error propagation through component layers.""" class DataLayer: def fetch_data(self, raise_error=False): if raise_error: raise ConnectionError("Database connection failed") return {"data": "test"} class ServiceLayer: def __init__(self, data_layer, error_handler): self.data_layer = data_layer self.error_handler = error_handler def get_data(self, raise_error=False): try: return self.data_layer.fetch_data(raise_error) except Exception as e: return self.error_handler.handle_error(e, context="service_layer") class ApiLayer: def __init__(self, service_layer, error_handler): self.service_layer = service_layer self.error_handler = error_handler def api_get_data(self, raise_error=False): try: result = self.service_layer.get_data(raise_error) if result.get("error"): return {"success": False, "error": result["error"]} return {"success": True, "data": result} except Exception as e: error_response = self.error_handler.handle_error(e, context="api_layer") return {"success": False, "error": error_response["error"]} # Mock error handler class ErrorHandler: def __init__(self): self.handled_errors = [] def handle_error(self, error, context=None): error_info = { "error_type": type(error).__name__, "error": str(error), "context": context, "handled": True } self.handled_errors.append(error_info) return error_info # Set up components error_handler = ErrorHandler() data_layer = DataLayer() service_layer = ServiceLayer(data_layer, error_handler) api_layer = ApiLayer(service_layer, error_handler) # Test successful execution result = api_layer.api_get_data(raise_error=False) assert result["success"] is True assert result["data"]["data"] == "test" # Test error propagation result = api_layer.api_get_data(raise_error=True) assert result["success"] is False assert "Database connection failed" in result["error"] # Verify error was handled at service layer assert len(error_handler.handled_errors) == 1 assert error_handler.handled_errors[0]["context"] == "service_layer" assert error_handler.handled_errors[0]["error_type"] == "ConnectionError" def test_error_recovery_integration(self): """Test error recovery integration across components.""" class RetryableService: def __init__(self, max_retries=3): self.max_retries = max_retries self.attempt_count = 0 def unreliable_operation(self): self.attempt_count += 1 if self.attempt_count < 3: raise ConnectionError(f"Attempt {self.attempt_count} failed") return f"Success on attempt {self.attempt_count}" def execute_with_retry(service, operation_name, max_retries=3): """Execute operation with retry logic.""" last_error = None for attempt in range(max_retries): try: operation = getattr(service, operation_name) return operation() except Exception as e: last_error = e if attempt == max_retries - 1: raise e raise last_error # Test successful retry service = RetryableService() result = execute_with_retry(service, "unreliable_operation") assert "Success on attempt 3" in result # Test failure after max retries service = RetryableService(max_retries=10) # Will fail more than 3 times with pytest.raises(ConnectionError): execute_with_retry(service, "unreliable_operation", max_retries=2) @pytest.mark.integration class TestModularArchitectureIntegration: """Test modular architecture integration.""" def test_provider_system_integration(self): """Test complete provider system integration.""" # Mock provider implementations class BaseProvider: def search(self, query): raise NotImplementedError class AniworldProvider(BaseProvider): def search(self, query): return [{"title": f"Aniworld: {query}", "source": "aniworld"}] class BackupProvider(BaseProvider): def search(self, query): return [{"title": f"Backup: {query}", "source": "backup"}] # Provider factory class ProviderFactory: def __init__(self): self.providers = {} def register(self, name, provider_class): self.providers[name] = provider_class def create(self, name): if name not in self.providers: raise ValueError(f"Provider {name} not found") return self.providers[name]() # Provider service with fallback class ProviderService: def __init__(self, factory, primary_provider, fallback_providers=None): self.factory = factory self.primary_provider = primary_provider self.fallback_providers = fallback_providers or [] def search(self, query): # Try primary provider try: provider = self.factory.create(self.primary_provider) return provider.search(query) except Exception: # Try fallback providers for fallback_name in self.fallback_providers: try: provider = self.factory.create(fallback_name) return provider.search(query) except Exception: continue raise Exception("All providers failed") # Set up provider system factory = ProviderFactory() factory.register("aniworld", AniworldProvider) factory.register("backup", BackupProvider) service = ProviderService( factory, primary_provider="aniworld", fallback_providers=["backup"] ) # Test primary provider success results = service.search("test anime") assert len(results) == 1 assert results[0]["source"] == "aniworld" # Test fallback when primary fails factory.register("failing", lambda: None) # Will fail on search service_with_failing_primary = ProviderService( factory, primary_provider="failing", fallback_providers=["backup"] ) results = service_with_failing_primary.search("test anime") assert len(results) == 1 assert results[0]["source"] == "backup" def test_repository_service_integration(self): """Test repository and service layer integration.""" # Mock repository class AnimeRepository: def __init__(self): self.data = {} self.next_id = 1 def save(self, anime): anime_id = self.next_id self.next_id += 1 anime_data = {**anime, "id": anime_id} self.data[anime_id] = anime_data return anime_data def find_by_id(self, anime_id): return self.data.get(anime_id) def find_all(self): return list(self.data.values()) def find_by_title(self, title): return [anime for anime in self.data.values() if title.lower() in anime["title"].lower()] # Service layer class AnimeService: def __init__(self, repository, provider_service): self.repository = repository self.provider_service = provider_service def search_and_cache(self, query): # Check cache first cached = self.repository.find_by_title(query) if cached: return {"source": "cache", "results": cached} # Search using provider results = self.provider_service.search(query) # Cache results cached_results = [] for result in results: saved = self.repository.save(result) cached_results.append(saved) return {"source": "provider", "results": cached_results} # Mock provider service mock_provider = Mock() mock_provider.search.return_value = [ {"title": "Test Anime", "genre": "Action"} ] # Set up service repository = AnimeRepository() service = AnimeService(repository, mock_provider) # First search should use provider result1 = service.search_and_cache("Test") assert result1["source"] == "provider" assert len(result1["results"]) == 1 assert result1["results"][0]["id"] == 1 # Second search should use cache result2 = service.search_and_cache("Test") assert result2["source"] == "cache" assert len(result2["results"]) == 1 assert result2["results"][0]["id"] == 1 # Verify provider was only called once mock_provider.search.assert_called_once_with("Test") def test_event_driven_integration(self): """Test event-driven component integration.""" # Event bus class EventBus: def __init__(self): self.subscribers = {} def subscribe(self, event_type, handler): if event_type not in self.subscribers: self.subscribers[event_type] = [] self.subscribers[event_type].append(handler) def publish(self, event_type, data): if event_type in self.subscribers: for handler in self.subscribers[event_type]: handler(data) # Components that publish/subscribe to events class DownloadService: def __init__(self, event_bus): self.event_bus = event_bus def download_anime(self, anime_id): # Simulate download self.event_bus.publish("download_started", {"anime_id": anime_id}) # Simulate completion self.event_bus.publish("download_completed", { "anime_id": anime_id, "status": "success" }) class NotificationService: def __init__(self, event_bus): self.event_bus = event_bus self.notifications = [] # Subscribe to events self.event_bus.subscribe("download_started", self.on_download_started) self.event_bus.subscribe("download_completed", self.on_download_completed) def on_download_started(self, data): self.notifications.append(f"Download started for anime {data['anime_id']}") def on_download_completed(self, data): self.notifications.append(f"Download completed for anime {data['anime_id']}") class StatisticsService: def __init__(self, event_bus): self.event_bus = event_bus self.download_count = 0 self.completed_count = 0 # Subscribe to events self.event_bus.subscribe("download_started", self.on_download_started) self.event_bus.subscribe("download_completed", self.on_download_completed) def on_download_started(self, data): self.download_count += 1 def on_download_completed(self, data): self.completed_count += 1 # Set up event-driven system event_bus = EventBus() download_service = DownloadService(event_bus) notification_service = NotificationService(event_bus) stats_service = StatisticsService(event_bus) # Trigger download download_service.download_anime(123) # Verify events were handled assert len(notification_service.notifications) == 2 assert "Download started for anime 123" in notification_service.notifications assert "Download completed for anime 123" in notification_service.notifications assert stats_service.download_count == 1 assert stats_service.completed_count == 1