Aniworld/src/tests/integration/test_misc_integration.py
Lukas Pupka-Lipinski a93c787031 Add miscellaneous component tests - environment config, error handling, and modular architecture
- Unit tests for environment configuration loading and validation
- Error handling pipelines and recovery strategies
- Modular architecture patterns (factory, dependency injection, repository)
- Integration tests for configuration propagation and error handling
- Event-driven component integration testing
- Repository-service layer integration
- Provider system with fallback functionality
2025-10-06 11:21:54 +02:00

521 lines
20 KiB
Python

"""
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