teachingAssistant / tests /unit /application /services /test_configuration_service.py
Michael Hu
Create unit tests for application layer
acd758a
"""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()