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