"""Unit tests for ConfigurationApplicationService""" import pytest import os import json from unittest.mock import Mock, MagicMock, patch, mock_open from src.application.services.configuration_service import ( ConfigurationApplicationService, ConfigurationException ) from src.infrastructure.config.app_config import AppConfig from src.infrastructure.config.dependency_container import DependencyContainer class TestConfigurationApplicationService: """Test cases for ConfigurationApplicationService""" @pytest.fixture def mock_container(self): """Create mock dependency container""" container = Mock(spec=DependencyContainer) return container @pytest.fixture def mock_config(self): """Create mock application config""" config = Mock(spec=AppConfig) # Mock configuration methods config.get_tts_config.return_value = { 'preferred_providers': ['kokoro', 'dia'], 'default_speed': 1.0, 'default_language': 'en', 'enable_streaming': False, 'max_text_length': 5000 } config.get_stt_config.return_value = { 'preferred_providers': ['whisper', 'parakeet'], 'default_model': 'whisper', 'chunk_length_s': 30, 'batch_size': 16, 'enable_vad': True } config.get_translation_config.return_value = { 'default_provider': 'nllb', 'model_name': 'nllb-200-3.3B', 'max_chunk_length': 512, 'batch_size': 8, 'cache_translations': True } config.get_processing_config.return_value = { 'temp_dir': '/tmp', 'cleanup_temp_files': True, 'max_file_size_mb': 100, 'supported_audio_formats': ['wav', 'mp3', 'flac'], 'processing_timeout_seconds': 300 } config.get_logging_config.return_value = { 'level': 'INFO', 'enable_file_logging': False, 'log_file_path': '/tmp/app.log', 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s' } # Mock config objects for attribute access config.tts = Mock() config.stt = Mock() config.translation = Mock() config.processing = Mock() config.logging = Mock() return config @pytest.fixture def service(self, mock_container, mock_config): """Create ConfigurationApplicationService instance""" mock_container.resolve.return_value = mock_config return ConfigurationApplicationService(mock_container, mock_config) def test_initialization(self, mock_container, mock_config): """Test service initialization""" service = ConfigurationApplicationService(mock_container, mock_config) assert service._container == mock_container assert service._config == mock_config assert service._error_mapper is not None def test_initialization_without_config(self, mock_container, mock_config): """Test service initialization without explicit config""" mock_container.resolve.return_value = mock_config service = ConfigurationApplicationService(mock_container) assert service._container == mock_container assert service._config == mock_config mock_container.resolve.assert_called_once_with(AppConfig) def test_get_current_configuration_success(self, service, mock_config): """Test successful current configuration retrieval""" result = service.get_current_configuration() assert 'tts' in result assert 'stt' in result assert 'translation' in result assert 'processing' in result assert 'logging' in result # Verify all config methods were called mock_config.get_tts_config.assert_called_once() mock_config.get_stt_config.assert_called_once() mock_config.get_translation_config.assert_called_once() mock_config.get_processing_config.assert_called_once() mock_config.get_logging_config.assert_called_once() def test_get_current_configuration_failure(self, service, mock_config): """Test current configuration retrieval failure""" mock_config.get_tts_config.side_effect = Exception("Config error") with pytest.raises(ConfigurationException, match="Failed to retrieve configuration"): service.get_current_configuration() def test_get_tts_configuration_success(self, service, mock_config): """Test successful TTS configuration retrieval""" result = service.get_tts_configuration() assert result['preferred_providers'] == ['kokoro', 'dia'] assert result['default_speed'] == 1.0 mock_config.get_tts_config.assert_called_once() def test_get_tts_configuration_failure(self, service, mock_config): """Test TTS configuration retrieval failure""" mock_config.get_tts_config.side_effect = Exception("TTS config error") with pytest.raises(ConfigurationException, match="Failed to retrieve TTS configuration"): service.get_tts_configuration() def test_get_stt_configuration_success(self, service, mock_config): """Test successful STT configuration retrieval""" result = service.get_stt_configuration() assert result['preferred_providers'] == ['whisper', 'parakeet'] assert result['default_model'] == 'whisper' mock_config.get_stt_config.assert_called_once() def test_get_stt_configuration_failure(self, service, mock_config): """Test STT configuration retrieval failure""" mock_config.get_stt_config.side_effect = Exception("STT config error") with pytest.raises(ConfigurationException, match="Failed to retrieve STT configuration"): service.get_stt_configuration() def test_get_translation_configuration_success(self, service, mock_config): """Test successful translation configuration retrieval""" result = service.get_translation_configuration() assert result['default_provider'] == 'nllb' assert result['model_name'] == 'nllb-200-3.3B' mock_config.get_translation_config.assert_called_once() def test_get_translation_configuration_failure(self, service, mock_config): """Test translation configuration retrieval failure""" mock_config.get_translation_config.side_effect = Exception("Translation config error") with pytest.raises(ConfigurationException, match="Failed to retrieve translation configuration"): service.get_translation_configuration() def test_get_processing_configuration_success(self, service, mock_config): """Test successful processing configuration retrieval""" result = service.get_processing_configuration() assert result['temp_dir'] == '/tmp' assert result['max_file_size_mb'] == 100 mock_config.get_processing_config.assert_called_once() def test_get_processing_configuration_failure(self, service, mock_config): """Test processing configuration retrieval failure""" mock_config.get_processing_config.side_effect = Exception("Processing config error") with pytest.raises(ConfigurationException, match="Failed to retrieve processing configuration"): service.get_processing_configuration() def test_get_logging_configuration_success(self, service, mock_config): """Test successful logging configuration retrieval""" result = service.get_logging_configuration() assert result['level'] == 'INFO' assert result['enable_file_logging'] is False mock_config.get_logging_config.assert_called_once() def test_get_logging_configuration_failure(self, service, mock_config): """Test logging configuration retrieval failure""" mock_config.get_logging_config.side_effect = Exception("Logging config error") with pytest.raises(ConfigurationException, match="Failed to retrieve logging configuration"): service.get_logging_configuration() def test_update_tts_configuration_success(self, service, mock_config): """Test successful TTS configuration update""" updates = { 'default_speed': 1.5, 'enable_streaming': True } result = service.update_tts_configuration(updates) # Verify setattr was called for valid attributes assert hasattr(mock_config.tts, 'default_speed') assert hasattr(mock_config.tts, 'enable_streaming') # Verify updated config was returned mock_config.get_tts_config.assert_called() def test_update_tts_configuration_validation_error(self, service): """Test TTS configuration update with validation error""" updates = { 'default_speed': 5.0 # Invalid speed > 3.0 } with pytest.raises(ConfigurationException, match="default_speed must be between 0.1 and 3.0"): service.update_tts_configuration(updates) def test_update_stt_configuration_success(self, service, mock_config): """Test successful STT configuration update""" updates = { 'chunk_length_s': 60, 'enable_vad': False } result = service.update_stt_configuration(updates) # Verify setattr was called for valid attributes assert hasattr(mock_config.stt, 'chunk_length_s') assert hasattr(mock_config.stt, 'enable_vad') # Verify updated config was returned mock_config.get_stt_config.assert_called() def test_update_stt_configuration_validation_error(self, service): """Test STT configuration update with validation error""" updates = { 'chunk_length_s': -10 # Invalid negative value } with pytest.raises(ConfigurationException, match="chunk_length_s must be a positive integer"): service.update_stt_configuration(updates) def test_update_translation_configuration_success(self, service, mock_config): """Test successful translation configuration update""" updates = { 'max_chunk_length': 1024, 'cache_translations': False } result = service.update_translation_configuration(updates) # Verify setattr was called for valid attributes assert hasattr(mock_config.translation, 'max_chunk_length') assert hasattr(mock_config.translation, 'cache_translations') # Verify updated config was returned mock_config.get_translation_config.assert_called() def test_update_translation_configuration_validation_error(self, service): """Test translation configuration update with validation error""" updates = { 'max_chunk_length': 0 # Invalid zero value } with pytest.raises(ConfigurationException, match="max_chunk_length must be a positive integer"): service.update_translation_configuration(updates) def test_update_processing_configuration_success(self, service, mock_config): """Test successful processing configuration update""" updates = { 'max_file_size_mb': 200, 'cleanup_temp_files': False } with patch('pathlib.Path.mkdir'): result = service.update_processing_configuration(updates) # Verify setattr was called for valid attributes assert hasattr(mock_config.processing, 'max_file_size_mb') assert hasattr(mock_config.processing, 'cleanup_temp_files') # Verify updated config was returned mock_config.get_processing_config.assert_called() def test_update_processing_configuration_validation_error(self, service): """Test processing configuration update with validation error""" updates = { 'max_file_size_mb': -50 # Invalid negative value } with pytest.raises(ConfigurationException, match="max_file_size_mb must be a positive integer"): service.update_processing_configuration(updates) def test_validate_tts_updates_valid(self, service): """Test TTS updates validation with valid data""" updates = { 'preferred_providers': ['kokoro', 'dia'], 'default_speed': 1.5, 'default_language': 'es', 'enable_streaming': True, 'max_text_length': 10000 } # Should not raise exception service._validate_tts_updates(updates) def test_validate_tts_updates_invalid_provider(self, service): """Test TTS updates validation with invalid provider""" updates = { 'preferred_providers': ['invalid_provider'] } with pytest.raises(ConfigurationException, match="Invalid TTS provider"): service._validate_tts_updates(updates) def test_validate_tts_updates_invalid_speed(self, service): """Test TTS updates validation with invalid speed""" updates = { 'default_speed': 5.0 # Too high } with pytest.raises(ConfigurationException, match="default_speed must be between 0.1 and 3.0"): service._validate_tts_updates(updates) def test_validate_stt_updates_valid(self, service): """Test STT updates validation with valid data""" updates = { 'preferred_providers': ['whisper', 'parakeet'], 'default_model': 'whisper', 'chunk_length_s': 45, 'batch_size': 32, 'enable_vad': False } # Should not raise exception service._validate_stt_updates(updates) def test_validate_stt_updates_invalid_provider(self, service): """Test STT updates validation with invalid provider""" updates = { 'preferred_providers': ['invalid_stt'] } with pytest.raises(ConfigurationException, match="Invalid STT provider"): service._validate_stt_updates(updates) def test_validate_translation_updates_valid(self, service): """Test translation updates validation with valid data""" updates = { 'default_provider': 'nllb', 'model_name': 'nllb-200-1.3B', 'max_chunk_length': 256, 'batch_size': 4, 'cache_translations': False } # Should not raise exception service._validate_translation_updates(updates) def test_validate_processing_updates_valid(self, service): """Test processing updates validation with valid data""" updates = { 'temp_dir': '/tmp/test', 'cleanup_temp_files': True, 'max_file_size_mb': 150, 'supported_audio_formats': ['wav', 'mp3'], 'processing_timeout_seconds': 600 } with patch('pathlib.Path.mkdir'): # Should not raise exception service._validate_processing_updates(updates) def test_validate_processing_updates_invalid_format(self, service): """Test processing updates validation with invalid audio format""" updates = { 'supported_audio_formats': ['wav', 'invalid_format'] } with pytest.raises(ConfigurationException, match="Invalid audio format"): service._validate_processing_updates(updates) def test_save_configuration_to_file_success(self, service, mock_config): """Test successful configuration save to file""" file_path = "/tmp/config.json" service.save_configuration_to_file(file_path) mock_config.save_configuration.assert_called_once_with(file_path) def test_save_configuration_to_file_failure(self, service, mock_config): """Test configuration save to file failure""" file_path = "/tmp/config.json" mock_config.save_configuration.side_effect = Exception("Save failed") with pytest.raises(ConfigurationException, match="Failed to save configuration"): service.save_configuration_to_file(file_path) @patch('os.path.exists') def test_load_configuration_from_file_success(self, mock_exists, service, mock_container): """Test successful configuration load from file""" file_path = "/tmp/config.json" mock_exists.return_value = True with patch('src.infrastructure.config.app_config.AppConfig') as mock_app_config: new_config = Mock() mock_app_config.return_value = new_config result = service.load_configuration_from_file(file_path) # Verify new config was created and registered mock_app_config.assert_called_once_with(config_file=file_path) mock_container.register_singleton.assert_called_once_with(AppConfig, new_config) @patch('os.path.exists') def test_load_configuration_from_file_not_found(self, mock_exists, service): """Test configuration load from non-existent file""" file_path = "/tmp/nonexistent.json" mock_exists.return_value = False with pytest.raises(ConfigurationException, match="Configuration file not found"): service.load_configuration_from_file(file_path) def test_reload_configuration_success(self, service, mock_config): """Test successful configuration reload""" result = service.reload_configuration() mock_config.reload_configuration.assert_called_once() assert 'tts' in result def test_reload_configuration_failure(self, service, mock_config): """Test configuration reload failure""" mock_config.reload_configuration.side_effect = Exception("Reload failed") with pytest.raises(ConfigurationException, match="Failed to reload configuration"): service.reload_configuration() def test_get_provider_availability(self, service, mock_container): """Test provider availability check""" # Mock factories mock_tts_factory = Mock() mock_stt_factory = Mock() mock_translation_factory = Mock() mock_container.resolve.side_effect = [mock_tts_factory, mock_stt_factory, mock_translation_factory] # Mock successful provider creation mock_tts_factory.create_provider.return_value = Mock() mock_stt_factory.create_provider.return_value = Mock() mock_translation_factory.get_default_provider.return_value = Mock() result = service.get_provider_availability() assert 'tts' in result assert 'stt' in result assert 'translation' in result # All providers should be available assert all(result['tts'].values()) assert all(result['stt'].values()) assert result['translation']['nllb'] is True def test_get_system_info(self, service, mock_config): """Test system information retrieval""" mock_config.config_file = "/tmp/config.json" mock_config.processing.temp_dir = "/tmp" mock_config.logging.level = "INFO" mock_config.processing.supported_audio_formats = ['wav', 'mp3'] mock_config.processing.max_file_size_mb = 100 mock_config.processing.processing_timeout_seconds = 300 with patch.object(service, 'get_provider_availability', return_value={}): result = service.get_system_info() assert result['config_file'] == "/tmp/config.json" assert result['temp_directory'] == "/tmp" assert result['log_level'] == "INFO" assert 'supported_languages' in result assert 'supported_audio_formats' in result assert 'max_file_size_mb' in result def test_validate_configuration_success(self, service, mock_config): """Test successful configuration validation""" # Mock valid configuration mock_config.get_tts_config.return_value = { 'default_speed': 1.0, 'max_text_length': 5000 } mock_config.get_stt_config.return_value = { 'chunk_length_s': 30, 'batch_size': 16 } mock_config.get_processing_config.return_value = { 'temp_dir': '/tmp', 'max_file_size_mb': 100 } mock_config.get_logging_config.return_value = { 'level': 'INFO' } with patch('os.path.exists', return_value=True): result = service.validate_configuration() # Should have no issues assert all(len(issues) == 0 for issues in result.values()) def test_validate_configuration_with_issues(self, service, mock_config): """Test configuration validation with issues""" # Mock invalid configuration mock_config.get_tts_config.return_value = { 'default_speed': 5.0, # Invalid 'max_text_length': -100 # Invalid } mock_config.get_stt_config.return_value = { 'chunk_length_s': -10, # Invalid 'batch_size': 0 # Invalid } mock_config.get_processing_config.return_value = { 'temp_dir': '/nonexistent', # Invalid 'max_file_size_mb': -50 # Invalid } mock_config.get_logging_config.return_value = { 'level': 'INVALID' # Invalid } with patch('os.path.exists', return_value=False): result = service.validate_configuration() # Should have issues in each category assert len(result['tts']) > 0 assert len(result['stt']) > 0 assert len(result['processing']) > 0 assert len(result['logging']) > 0 def test_reset_to_defaults_success(self, service, mock_container): """Test successful configuration reset to defaults""" with patch('src.infrastructure.config.app_config.AppConfig') as mock_app_config: default_config = Mock() mock_app_config.return_value = default_config result = service.reset_to_defaults() # Verify new default config was created and registered mock_app_config.assert_called_once_with() mock_container.register_singleton.assert_called_once_with(AppConfig, default_config) def test_reset_to_defaults_failure(self, service): """Test configuration reset to defaults failure""" with patch('src.infrastructure.config.app_config.AppConfig', side_effect=Exception("Reset failed")): with pytest.raises(ConfigurationException, match="Failed to reset configuration"): service.reset_to_defaults() def test_cleanup(self, service): """Test service cleanup""" # Should not raise exception service.cleanup() def test_context_manager(self, service): """Test service as context manager""" with patch.object(service, 'cleanup') as mock_cleanup: with service as svc: assert svc == service mock_cleanup.assert_called_once()