- 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
521 lines
20 KiB
Python
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 |