Michael Hu commited on
Commit
5b02cb3
·
1 Parent(s): 8023ba2

remove tests

Browse files
Files changed (43) hide show
  1. tests/integration/__init__.py +0 -1
  2. tests/integration/test_audio_processing_pipeline.py +0 -452
  3. tests/integration/test_file_handling.py +0 -580
  4. tests/integration/test_performance_and_errors.py +0 -550
  5. tests/integration/test_provider_integration.py +0 -462
  6. tests/unit/application/__init__.py +0 -1
  7. tests/unit/application/dtos/__init__.py +0 -1
  8. tests/unit/application/dtos/test_audio_upload_dto.py +0 -245
  9. tests/unit/application/dtos/test_dto_validation.py +0 -319
  10. tests/unit/application/dtos/test_processing_request_dto.py +0 -383
  11. tests/unit/application/dtos/test_processing_result_dto.py +0 -436
  12. tests/unit/application/error_handling/__init__.py +0 -1
  13. tests/unit/application/error_handling/test_error_mapper.py +0 -155
  14. tests/unit/application/error_handling/test_structured_logger.py +0 -298
  15. tests/unit/application/services/__init__.py +0 -1
  16. tests/unit/application/services/test_audio_processing_service.py +0 -475
  17. tests/unit/application/services/test_configuration_service.py +0 -572
  18. tests/unit/domain/interfaces/__init__.py +0 -1
  19. tests/unit/domain/interfaces/test_audio_processing.py +0 -212
  20. tests/unit/domain/interfaces/test_speech_recognition.py +0 -241
  21. tests/unit/domain/interfaces/test_speech_synthesis.py +0 -378
  22. tests/unit/domain/interfaces/test_translation.py +0 -303
  23. tests/unit/domain/models/test_audio_chunk.py +0 -322
  24. tests/unit/domain/models/test_audio_content.py +0 -229
  25. tests/unit/domain/models/test_processing_result.py +0 -411
  26. tests/unit/domain/models/test_speech_synthesis_request.py +0 -323
  27. tests/unit/domain/models/test_text_content.py +0 -240
  28. tests/unit/domain/models/test_translation_request.py +0 -243
  29. tests/unit/domain/models/test_voice_settings.py +0 -408
  30. tests/unit/domain/services/__init__.py +0 -1
  31. tests/unit/domain/services/test_audio_processing_service.py +0 -297
  32. tests/unit/domain/test_exceptions.py +0 -240
  33. tests/unit/infrastructure/__init__.py +0 -1
  34. tests/unit/infrastructure/base/__init__.py +0 -1
  35. tests/unit/infrastructure/base/test_stt_provider_base.py +0 -359
  36. tests/unit/infrastructure/base/test_translation_provider_base.py +0 -325
  37. tests/unit/infrastructure/base/test_tts_provider_base.py +0 -297
  38. tests/unit/infrastructure/config/__init__.py +0 -1
  39. tests/unit/infrastructure/config/test_dependency_container.py +0 -539
  40. tests/unit/infrastructure/factories/__init__.py +0 -1
  41. tests/unit/infrastructure/factories/test_stt_provider_factory.py +0 -284
  42. tests/unit/infrastructure/factories/test_translation_provider_factory.py +0 -346
  43. tests/unit/infrastructure/factories/test_tts_provider_factory.py +0 -232
tests/integration/__init__.py DELETED
@@ -1 +0,0 @@
1
- # Integration tests package
 
 
tests/integration/test_audio_processing_pipeline.py DELETED
@@ -1,452 +0,0 @@
1
- """Integration tests for the complete audio processing pipeline."""
2
-
3
- import os
4
- import tempfile
5
- import time
6
- import pytest
7
- from pathlib import Path
8
- from unittest.mock import Mock, patch, MagicMock
9
- from typing import Dict, Any, Optional
10
-
11
- from src.application.services.audio_processing_service import AudioProcessingApplicationService
12
- from src.application.dtos.audio_upload_dto import AudioUploadDto
13
- from src.application.dtos.processing_request_dto import ProcessingRequestDto
14
- from src.application.dtos.processing_result_dto import ProcessingResultDto
15
- from src.infrastructure.config.dependency_container import DependencyContainer
16
- from src.infrastructure.config.app_config import AppConfig
17
- from src.domain.models.audio_content import AudioContent
18
- from src.domain.models.text_content import TextContent
19
- from src.domain.models.voice_settings import VoiceSettings
20
- from src.domain.exceptions import (
21
- SpeechRecognitionException,
22
- TranslationFailedException,
23
- SpeechSynthesisException
24
- )
25
-
26
-
27
- class TestAudioProcessingPipeline:
28
- """Integration tests for the complete audio processing pipeline."""
29
-
30
- @pytest.fixture
31
- def temp_dir(self):
32
- """Create temporary directory for test files."""
33
- with tempfile.TemporaryDirectory() as temp_dir:
34
- yield temp_dir
35
-
36
- @pytest.fixture
37
- def mock_config(self, temp_dir):
38
- """Create mock configuration for testing."""
39
- config = Mock(spec=AppConfig)
40
-
41
- # Processing configuration
42
- config.get_processing_config.return_value = {
43
- 'max_file_size_mb': 50,
44
- 'supported_audio_formats': ['wav', 'mp3', 'flac'],
45
- 'temp_dir': temp_dir,
46
- 'cleanup_temp_files': True
47
- }
48
-
49
- # Logging configuration
50
- config.get_logging_config.return_value = {
51
- 'level': 'INFO',
52
- 'enable_file_logging': False,
53
- 'log_file_path': os.path.join(temp_dir, 'test.log'),
54
- 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
55
- }
56
-
57
- # STT configuration
58
- config.get_stt_config.return_value = {
59
- 'preferred_providers': ['parakeet', 'whisper-small', 'whisper-medium']
60
- }
61
-
62
- # TTS configuration
63
- config.get_tts_config.return_value = {
64
- 'preferred_providers': ['chatterbox']
65
- }
66
-
67
- return config
68
-
69
- @pytest.fixture
70
- def mock_container(self, mock_config):
71
- """Create mock dependency container for testing."""
72
- container = Mock(spec=DependencyContainer)
73
- container.resolve.return_value = mock_config
74
-
75
- # Mock STT provider
76
- mock_stt_provider = Mock()
77
- mock_stt_provider.transcribe.return_value = TextContent(
78
- text="Hello, this is a test transcription.",
79
- language="en"
80
- )
81
- container.get_stt_provider.return_value = mock_stt_provider
82
-
83
- # Mock translation provider
84
- mock_translation_provider = Mock()
85
- mock_translation_provider.translate.return_value = TextContent(
86
- text="Hola, esta es una transcripción de prueba.",
87
- language="es"
88
- )
89
- container.get_translation_provider.return_value = mock_translation_provider
90
-
91
- # Mock TTS provider
92
- mock_tts_provider = Mock()
93
- mock_audio_content = AudioContent(
94
- data=b"fake_audio_data",
95
- format="wav",
96
- sample_rate=22050,
97
- duration=2.5
98
- )
99
- mock_tts_provider.synthesize.return_value = mock_audio_content
100
- container.get_tts_provider.return_value = mock_tts_provider
101
-
102
- return container
103
-
104
- @pytest.fixture
105
- def audio_service(self, mock_container, mock_config):
106
- """Create audio processing service for testing."""
107
- return AudioProcessingApplicationService(mock_container, mock_config)
108
-
109
- @pytest.fixture
110
- def sample_audio_upload(self):
111
- """Create sample audio upload DTO."""
112
- return AudioUploadDto(
113
- filename="test_audio.wav",
114
- content=b"fake_wav_audio_data",
115
- content_type="audio/wav",
116
- size=1024
117
- )
118
-
119
- @pytest.fixture
120
- def sample_processing_request(self, sample_audio_upload):
121
- """Create sample processing request DTO."""
122
- return ProcessingRequestDto(
123
- audio=sample_audio_upload,
124
- asr_model="whisper-small",
125
- target_language="es",
126
- source_language="en",
127
- voice="chatterbox",
128
- speed=1.0,
129
- requires_translation=True
130
- )
131
-
132
- def test_complete_pipeline_success(self, audio_service, sample_processing_request):
133
- """Test successful execution of the complete audio processing pipeline."""
134
- # Execute the pipeline
135
- result = audio_service.process_audio_pipeline(sample_processing_request)
136
-
137
- # Verify successful result
138
- assert isinstance(result, ProcessingResultDto)
139
- assert result.success is True
140
- assert result.error_message is None
141
- assert result.original_text == "Hello, this is a test transcription."
142
- assert result.translated_text == "Hola, esta es una transcripción de prueba."
143
- assert result.audio_path is not None
144
- assert result.processing_time > 0
145
- assert result.metadata is not None
146
- assert 'correlation_id' in result.metadata
147
-
148
- def test_pipeline_without_translation(self, audio_service, sample_audio_upload):
149
- """Test pipeline execution without translation (same language)."""
150
- request = ProcessingRequestDto(
151
- audio=sample_audio_upload,
152
- asr_model="whisper-small",
153
- target_language="en",
154
- source_language="en",
155
- voice="chatterbox",
156
- speed=1.0,
157
- requires_translation=False
158
- )
159
-
160
- result = audio_service.process_audio_pipeline(request)
161
-
162
- assert result.success is True
163
- assert result.original_text == "Hello, this is a test transcription."
164
- assert result.translated_text is None # No translation performed
165
- assert result.audio_path is not None
166
-
167
- def test_pipeline_with_different_voice_settings(self, audio_service, sample_audio_upload):
168
- """Test pipeline with different voice settings."""
169
- request = ProcessingRequestDto(
170
- audio=sample_audio_upload,
171
- asr_model="whisper-medium",
172
- target_language="fr",
173
- source_language="en",
174
- voice="chatterbox",
175
- speed=1.5,
176
- requires_translation=True
177
- )
178
-
179
- result = audio_service.process_audio_pipeline(request)
180
-
181
- assert result.success is True
182
- assert result.metadata['voice'] == "chatterbox"
183
- assert result.metadata['speed'] == 1.5
184
- assert result.metadata['asr_model'] == "whisper-medium"
185
-
186
- def test_pipeline_performance_metrics(self, audio_service, sample_processing_request):
187
- """Test that pipeline captures performance metrics."""
188
- start_time = time.time()
189
- result = audio_service.process_audio_pipeline(sample_processing_request)
190
- end_time = time.time()
191
-
192
- assert result.success is True
193
- assert result.processing_time > 0
194
- assert result.processing_time <= (end_time - start_time) + 0.1 # Allow small margin
195
- assert 'correlation_id' in result.metadata
196
-
197
- def test_pipeline_with_large_file(self, audio_service, mock_config):
198
- """Test pipeline behavior with large audio files."""
199
- # Create large audio upload
200
- large_audio = AudioUploadDto(
201
- filename="large_audio.wav",
202
- content=b"x" * (10 * 1024 * 1024), # 10MB
203
- content_type="audio/wav",
204
- size=10 * 1024 * 1024
205
- )
206
-
207
- request = ProcessingRequestDto(
208
- audio=large_audio,
209
- asr_model="whisper-small",
210
- target_language="es",
211
- voice="chatterbox",
212
- speed=1.0,
213
- requires_translation=True
214
- )
215
-
216
- result = audio_service.process_audio_pipeline(request)
217
-
218
- assert result.success is True
219
- assert result.metadata['file_size'] == 10 * 1024 * 1024
220
-
221
- def test_pipeline_file_cleanup(self, audio_service, sample_processing_request, temp_dir):
222
- """Test that temporary files are properly cleaned up."""
223
- # Count files before processing
224
- files_before = len(list(Path(temp_dir).rglob("*")))
225
-
226
- result = audio_service.process_audio_pipeline(sample_processing_request)
227
-
228
- # Verify processing succeeded
229
- assert result.success is True
230
-
231
- # Verify cleanup occurred (no additional temp files)
232
- files_after = len(list(Path(temp_dir).rglob("*")))
233
- assert files_after <= files_before + 1 # Allow for output file
234
-
235
- def test_pipeline_correlation_id_tracking(self, audio_service, sample_processing_request):
236
- """Test that correlation IDs are properly tracked throughout the pipeline."""
237
- result = audio_service.process_audio_pipeline(sample_processing_request)
238
-
239
- assert result.success is True
240
- assert 'correlation_id' in result.metadata
241
-
242
- correlation_id = result.metadata['correlation_id']
243
- assert isinstance(correlation_id, str)
244
- assert len(correlation_id) > 0
245
-
246
- # Verify correlation ID is used in status tracking
247
- status = audio_service.get_processing_status(correlation_id)
248
- assert status['correlation_id'] == correlation_id
249
-
250
- def test_pipeline_metadata_completeness(self, audio_service, sample_processing_request):
251
- """Test that pipeline result contains complete metadata."""
252
- result = audio_service.process_audio_pipeline(sample_processing_request)
253
-
254
- assert result.success is True
255
- assert result.metadata is not None
256
-
257
- expected_metadata_keys = [
258
- 'correlation_id', 'asr_model', 'target_language',
259
- 'voice', 'speed', 'translation_required'
260
- ]
261
-
262
- for key in expected_metadata_keys:
263
- assert key in result.metadata
264
-
265
- def test_pipeline_supported_configurations(self, audio_service):
266
- """Test retrieval of supported pipeline configurations."""
267
- config = audio_service.get_supported_configurations()
268
-
269
- assert 'asr_models' in config
270
- assert 'voices' in config
271
- assert 'languages' in config
272
- assert 'audio_formats' in config
273
- assert 'max_file_size_mb' in config
274
- assert 'speed_range' in config
275
-
276
- assert isinstance(config['asr_models'], list)
277
- assert isinstance(config['voices'], list)
278
- assert isinstance(config['languages'], list)
279
- assert len(config['asr_models']) > 0
280
- assert len(config['voices']) > 0
281
-
282
- def test_pipeline_context_manager(self, mock_container, mock_config):
283
- """Test audio service as context manager."""
284
- with AudioProcessingApplicationService(mock_container, mock_config) as service:
285
- assert service is not None
286
-
287
- # Service should be usable within context
288
- config = service.get_supported_configurations()
289
- assert config is not None
290
-
291
- def test_pipeline_multiple_requests(self, audio_service, sample_audio_upload):
292
- """Test processing multiple requests in sequence."""
293
- requests = []
294
- for i in range(3):
295
- request = ProcessingRequestDto(
296
- audio=sample_audio_upload,
297
- asr_model="whisper-small",
298
- target_language="es",
299
- voice="chatterbox",
300
- speed=1.0,
301
- requires_translation=True
302
- )
303
- requests.append(request)
304
-
305
- results = []
306
- for request in requests:
307
- result = audio_service.process_audio_pipeline(request)
308
- results.append(result)
309
-
310
- # Verify all requests succeeded
311
- for result in results:
312
- assert result.success is True
313
- assert result.original_text is not None
314
- assert result.translated_text is not None
315
-
316
- # Verify each request has unique correlation ID
317
- correlation_ids = [r.metadata['correlation_id'] for r in results]
318
- assert len(set(correlation_ids)) == 3 # All unique
319
-
320
- def test_pipeline_concurrent_processing(self, audio_service, sample_processing_request):
321
- """Test pipeline behavior under concurrent processing."""
322
- import threading
323
- import queue
324
-
325
- results_queue = queue.Queue()
326
-
327
- def process_request():
328
- try:
329
- result = audio_service.process_audio_pipeline(sample_processing_request)
330
- results_queue.put(result)
331
- except Exception as e:
332
- results_queue.put(e)
333
-
334
- # Start multiple threads
335
- threads = []
336
- for _ in range(3):
337
- thread = threading.Thread(target=process_request)
338
- threads.append(thread)
339
- thread.start()
340
-
341
- # Wait for completion
342
- for thread in threads:
343
- thread.join()
344
-
345
- # Verify all results
346
- results = []
347
- while not results_queue.empty():
348
- result = results_queue.get()
349
- if isinstance(result, Exception):
350
- pytest.fail(f"Concurrent processing failed: {result}")
351
- results.append(result)
352
-
353
- assert len(results) == 3
354
- for result in results:
355
- assert result.success is True
356
-
357
- def test_pipeline_memory_usage(self, audio_service, sample_processing_request):
358
- """Test pipeline memory usage and cleanup."""
359
- import psutil
360
- import os
361
-
362
- process = psutil.Process(os.getpid())
363
- memory_before = process.memory_info().rss
364
-
365
- # Process multiple requests
366
- for _ in range(5):
367
- result = audio_service.process_audio_pipeline(sample_processing_request)
368
- assert result.success is True
369
-
370
- memory_after = process.memory_info().rss
371
- memory_increase = memory_after - memory_before
372
-
373
- # Memory increase should be reasonable (less than 50MB for test data)
374
- assert memory_increase < 50 * 1024 * 1024
375
-
376
- def test_pipeline_with_streaming_synthesis(self, audio_service, sample_processing_request, mock_container):
377
- """Test pipeline with streaming TTS synthesis."""
378
- # Mock streaming TTS provider
379
- mock_tts_provider = mock_container.get_tts_provider.return_value
380
-
381
- def mock_stream():
382
- for i in range(3):
383
- yield AudioContent(
384
- data=f"chunk_{i}".encode(),
385
- format="wav",
386
- sample_rate=22050,
387
- duration=0.5
388
- )
389
-
390
- mock_tts_provider.synthesize_stream.return_value = mock_stream()
391
-
392
- result = audio_service.process_audio_pipeline(sample_processing_request)
393
-
394
- assert result.success is True
395
- assert result.audio_path is not None
396
-
397
- def test_pipeline_configuration_validation(self, audio_service):
398
- """Test pipeline configuration validation."""
399
- config = audio_service.get_supported_configurations()
400
-
401
- # Verify configuration structure
402
- assert isinstance(config['asr_models'], list)
403
- assert isinstance(config['voices'], list)
404
- assert isinstance(config['languages'], list)
405
- assert isinstance(config['audio_formats'], list)
406
- assert isinstance(config['max_file_size_mb'], (int, float))
407
- assert isinstance(config['speed_range'], dict)
408
-
409
- # Verify speed range
410
- speed_range = config['speed_range']
411
- assert 'min' in speed_range
412
- assert 'max' in speed_range
413
- assert speed_range['min'] < speed_range['max']
414
- assert speed_range['min'] > 0
415
- assert speed_range['max'] <= 3.0
416
-
417
- def test_pipeline_error_recovery_logging(self, audio_service, sample_processing_request, mock_container):
418
- """Test that error recovery attempts are properly logged."""
419
- # Mock STT provider to fail first time, succeed second time
420
- mock_stt_provider = mock_container.get_stt_provider.return_value
421
- mock_stt_provider.transcribe.side_effect = [
422
- SpeechRecognitionException("First attempt failed"),
423
- TextContent(text="Recovered transcription", language="en")
424
- ]
425
-
426
- with patch('src.application.services.audio_processing_service.logger') as mock_logger:
427
- result = audio_service.process_audio_pipeline(sample_processing_request)
428
-
429
- assert result.success is True
430
- # Verify error and recovery were logged
431
- mock_logger.warning.assert_called()
432
- mock_logger.info.assert_called()
433
-
434
- def test_pipeline_end_to_end_timing(self, audio_service, sample_processing_request):
435
- """Test end-to-end pipeline timing and performance."""
436
- start_time = time.time()
437
- result = audio_service.process_audio_pipeline(sample_processing_request)
438
- end_time = time.time()
439
-
440
- total_time = end_time - start_time
441
-
442
- assert result.success is True
443
- assert result.processing_time > 0
444
- assert result.processing_time <= total_time
445
-
446
- # For mock providers, processing should be fast
447
- assert total_time < 5.0 # Should complete within 5 seconds
448
-
449
- # Verify timing metadata
450
- assert 'correlation_id' in result.metadata
451
- timing_info = result.metadata
452
- assert timing_info is not None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/integration/test_file_handling.py DELETED
@@ -1,580 +0,0 @@
1
- """Integration tests for file handling and cleanup."""
2
-
3
- import os
4
- import tempfile
5
- import shutil
6
- import time
7
- import pytest
8
- from pathlib import Path
9
- from unittest.mock import Mock, patch, MagicMock
10
- from typing import List, Dict, Any
11
-
12
- from src.application.services.audio_processing_service import AudioProcessingApplicationService
13
- from src.application.dtos.audio_upload_dto import AudioUploadDto
14
- from src.application.dtos.processing_request_dto import ProcessingRequestDto
15
- from src.infrastructure.config.dependency_container import DependencyContainer
16
- from src.infrastructure.config.app_config import AppConfig
17
- from src.domain.models.audio_content import AudioContent
18
- from src.domain.models.text_content import TextContent
19
-
20
-
21
- class TestFileHandling:
22
- """Integration tests for file handling and cleanup."""
23
-
24
- @pytest.fixture
25
- def temp_base_dir(self):
26
- """Create base temporary directory for all tests."""
27
- with tempfile.TemporaryDirectory() as temp_dir:
28
- yield temp_dir
29
-
30
- @pytest.fixture
31
- def mock_config(self, temp_base_dir):
32
- """Create mock configuration with temporary directories."""
33
- config = Mock(spec=AppConfig)
34
-
35
- # Processing configuration with temp directory
36
- config.get_processing_config.return_value = {
37
- 'max_file_size_mb': 50,
38
- 'supported_audio_formats': ['wav', 'mp3', 'flac', 'ogg'],
39
- 'temp_dir': temp_base_dir,
40
- 'cleanup_temp_files': True,
41
- 'max_temp_file_age_hours': 24,
42
- 'temp_file_prefix': 'audio_processing_'
43
- }
44
-
45
- # Logging configuration
46
- config.get_logging_config.return_value = {
47
- 'level': 'INFO',
48
- 'enable_file_logging': True,
49
- 'log_file_path': os.path.join(temp_base_dir, 'processing.log'),
50
- 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
51
- }
52
-
53
- # STT configuration
54
- config.get_stt_config.return_value = {
55
- 'preferred_providers': ['whisper-small']
56
- }
57
-
58
- # TTS configuration
59
- config.get_tts_config.return_value = {
60
- 'preferred_providers': ['chatterbox']
61
- }
62
-
63
- return config
64
-
65
- @pytest.fixture
66
- def mock_container(self, mock_config):
67
- """Create mock dependency container."""
68
- container = Mock(spec=DependencyContainer)
69
- container.resolve.return_value = mock_config
70
-
71
- # Mock providers
72
- mock_stt_provider = Mock()
73
- mock_stt_provider.transcribe.return_value = TextContent(
74
- text="Test transcription",
75
- language="en"
76
- )
77
- container.get_stt_provider.return_value = mock_stt_provider
78
-
79
- mock_translation_provider = Mock()
80
- mock_translation_provider.translate.return_value = TextContent(
81
- text="Prueba de transcripción",
82
- language="es"
83
- )
84
- container.get_translation_provider.return_value = mock_translation_provider
85
-
86
- mock_tts_provider = Mock()
87
- mock_tts_provider.synthesize.return_value = AudioContent(
88
- data=b"synthesized_audio_data",
89
- format="wav",
90
- sample_rate=22050,
91
- duration=2.0
92
- )
93
- container.get_tts_provider.return_value = mock_tts_provider
94
-
95
- return container
96
-
97
- @pytest.fixture
98
- def audio_service(self, mock_container, mock_config):
99
- """Create audio processing service."""
100
- return AudioProcessingApplicationService(mock_container, mock_config)
101
-
102
- @pytest.fixture
103
- def sample_audio_files(self, temp_base_dir):
104
- """Create sample audio files for testing."""
105
- files = {}
106
-
107
- # Create different audio file types
108
- audio_formats = {
109
- 'wav': b'RIFF\x24\x00\x00\x00WAVEfmt \x10\x00\x00\x00',
110
- 'mp3': b'\xff\xfb\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00',
111
- 'flac': b'fLaC\x00\x00\x00\x22\x10\x00\x10\x00',
112
- 'ogg': b'OggS\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00'
113
- }
114
-
115
- for format_name, header in audio_formats.items():
116
- file_path = os.path.join(temp_base_dir, f'test_audio.{format_name}')
117
- with open(file_path, 'wb') as f:
118
- f.write(header + b'\x00' * 1000) # Add some padding
119
- files[format_name] = file_path
120
-
121
- yield files
122
-
123
- # Cleanup
124
- for file_path in files.values():
125
- if os.path.exists(file_path):
126
- os.remove(file_path)
127
-
128
- def test_temp_directory_creation(self, audio_service, temp_base_dir):
129
- """Test temporary directory creation and structure."""
130
- # Create a processing request to trigger temp directory creation
131
- audio_upload = AudioUploadDto(
132
- filename="test.wav",
133
- content=b"fake_audio_data",
134
- content_type="audio/wav",
135
- size=len(b"fake_audio_data")
136
- )
137
-
138
- request = ProcessingRequestDto(
139
- audio=audio_upload,
140
- asr_model="whisper-small",
141
- target_language="es",
142
- voice="chatterbox",
143
- speed=1.0,
144
- requires_translation=True
145
- )
146
-
147
- # Process and check temp directory creation
148
- result = audio_service.process_audio_pipeline(request)
149
-
150
- assert result.success is True
151
-
152
- # Verify base temp directory exists
153
- assert os.path.exists(temp_base_dir)
154
- assert os.path.isdir(temp_base_dir)
155
-
156
- def test_input_file_handling(self, audio_service, sample_audio_files):
157
- """Test handling of different input audio file formats."""
158
- for format_name, file_path in sample_audio_files.items():
159
- with open(file_path, 'rb') as f:
160
- content = f.read()
161
-
162
- audio_upload = AudioUploadDto(
163
- filename=f"test.{format_name}",
164
- content=content,
165
- content_type=f"audio/{format_name}",
166
- size=len(content)
167
- )
168
-
169
- request = ProcessingRequestDto(
170
- audio=audio_upload,
171
- asr_model="whisper-small",
172
- target_language="en",
173
- voice="chatterbox",
174
- speed=1.0,
175
- requires_translation=False
176
- )
177
-
178
- result = audio_service.process_audio_pipeline(request)
179
-
180
- assert result.success is True, f"Failed to process {format_name} file"
181
- assert result.audio_path is not None
182
- assert os.path.exists(result.audio_path)
183
-
184
- def test_output_file_generation(self, audio_service, temp_base_dir):
185
- """Test output audio file generation."""
186
- audio_upload = AudioUploadDto(
187
- filename="input.wav",
188
- content=b"input_audio_data",
189
- content_type="audio/wav",
190
- size=len(b"input_audio_data")
191
- )
192
-
193
- request = ProcessingRequestDto(
194
- audio=audio_upload,
195
- asr_model="whisper-small",
196
- target_language="es",
197
- voice="chatterbox",
198
- speed=1.0,
199
- requires_translation=True
200
- )
201
-
202
- result = audio_service.process_audio_pipeline(request)
203
-
204
- assert result.success is True
205
- assert result.audio_path is not None
206
-
207
- # Verify output file exists and has content
208
- assert os.path.exists(result.audio_path)
209
- assert os.path.getsize(result.audio_path) > 0
210
-
211
- # Verify file is in expected location
212
- assert temp_base_dir in result.audio_path
213
-
214
- def test_temp_file_cleanup_success(self, audio_service, temp_base_dir):
215
- """Test temporary file cleanup after successful processing."""
216
- initial_files = set(os.listdir(temp_base_dir))
217
-
218
- audio_upload = AudioUploadDto(
219
- filename="cleanup_test.wav",
220
- content=b"cleanup_test_data",
221
- content_type="audio/wav",
222
- size=len(b"cleanup_test_data")
223
- )
224
-
225
- request = ProcessingRequestDto(
226
- audio=audio_upload,
227
- asr_model="whisper-small",
228
- target_language="es",
229
- voice="chatterbox",
230
- speed=1.0,
231
- requires_translation=True
232
- )
233
-
234
- result = audio_service.process_audio_pipeline(request)
235
-
236
- assert result.success is True
237
-
238
- # Check that temporary processing files are cleaned up
239
- # (output file should remain)
240
- final_files = set(os.listdir(temp_base_dir))
241
- new_files = final_files - initial_files
242
-
243
- # Should only have the output file and possibly log files
244
- assert len(new_files) <= 2 # output file + possible log file
245
-
246
- def test_temp_file_cleanup_on_error(self, audio_service, temp_base_dir, mock_container):
247
- """Test temporary file cleanup when processing fails."""
248
- # Mock STT provider to fail
249
- mock_stt_provider = mock_container.get_stt_provider.return_value
250
- mock_stt_provider.transcribe.side_effect = Exception("STT failed")
251
-
252
- initial_files = set(os.listdir(temp_base_dir))
253
-
254
- audio_upload = AudioUploadDto(
255
- filename="error_test.wav",
256
- content=b"error_test_data",
257
- content_type="audio/wav",
258
- size=len(b"error_test_data")
259
- )
260
-
261
- request = ProcessingRequestDto(
262
- audio=audio_upload,
263
- asr_model="whisper-small",
264
- target_language="es",
265
- voice="chatterbox",
266
- speed=1.0,
267
- requires_translation=True
268
- )
269
-
270
- result = audio_service.process_audio_pipeline(request)
271
-
272
- assert result.success is False
273
-
274
- # Verify cleanup occurred even on error
275
- final_files = set(os.listdir(temp_base_dir))
276
- new_files = final_files - initial_files
277
-
278
- # Should have minimal new files (possibly just log files)
279
- assert len(new_files) <= 1
280
-
281
- def test_large_file_handling(self, audio_service, temp_base_dir):
282
- """Test handling of large audio files."""
283
- # Create large audio content (5MB)
284
- large_content = b"x" * (5 * 1024 * 1024)
285
-
286
- audio_upload = AudioUploadDto(
287
- filename="large_file.wav",
288
- content=large_content,
289
- content_type="audio/wav",
290
- size=len(large_content)
291
- )
292
-
293
- request = ProcessingRequestDto(
294
- audio=audio_upload,
295
- asr_model="whisper-small",
296
- target_language="es",
297
- voice="chatterbox",
298
- speed=1.0,
299
- requires_translation=True
300
- )
301
-
302
- result = audio_service.process_audio_pipeline(request)
303
-
304
- assert result.success is True
305
- assert result.audio_path is not None
306
- assert os.path.exists(result.audio_path)
307
-
308
- def test_concurrent_file_handling(self, audio_service, temp_base_dir):
309
- """Test concurrent file handling and cleanup."""
310
- import threading
311
- import queue
312
-
313
- results_queue = queue.Queue()
314
-
315
- def process_file(file_id):
316
- try:
317
- audio_upload = AudioUploadDto(
318
- filename=f"concurrent_{file_id}.wav",
319
- content=f"concurrent_data_{file_id}".encode(),
320
- content_type="audio/wav",
321
- size=len(f"concurrent_data_{file_id}".encode())
322
- )
323
-
324
- request = ProcessingRequestDto(
325
- audio=audio_upload,
326
- asr_model="whisper-small",
327
- target_language="es",
328
- voice="chatterbox",
329
- speed=1.0,
330
- requires_translation=True
331
- )
332
-
333
- result = audio_service.process_audio_pipeline(request)
334
- results_queue.put((file_id, result))
335
- except Exception as e:
336
- results_queue.put((file_id, e))
337
-
338
- # Start multiple threads
339
- threads = []
340
- for i in range(3):
341
- thread = threading.Thread(target=process_file, args=(i,))
342
- threads.append(thread)
343
- thread.start()
344
-
345
- # Wait for completion
346
- for thread in threads:
347
- thread.join()
348
-
349
- # Verify results
350
- results = {}
351
- while not results_queue.empty():
352
- file_id, result = results_queue.get()
353
- if isinstance(result, Exception):
354
- pytest.fail(f"Concurrent processing failed for file {file_id}: {result}")
355
- results[file_id] = result
356
-
357
- assert len(results) == 3
358
- for file_id, result in results.items():
359
- assert result.success is True
360
- assert result.audio_path is not None
361
- assert os.path.exists(result.audio_path)
362
-
363
- def test_file_permission_handling(self, audio_service, temp_base_dir):
364
- """Test file permission handling."""
365
- audio_upload = AudioUploadDto(
366
- filename="permission_test.wav",
367
- content=b"permission_test_data",
368
- content_type="audio/wav",
369
- size=len(b"permission_test_data")
370
- )
371
-
372
- request = ProcessingRequestDto(
373
- audio=audio_upload,
374
- asr_model="whisper-small",
375
- target_language="es",
376
- voice="chatterbox",
377
- speed=1.0,
378
- requires_translation=True
379
- )
380
-
381
- result = audio_service.process_audio_pipeline(request)
382
-
383
- assert result.success is True
384
- assert result.audio_path is not None
385
-
386
- # Verify file permissions
387
- file_stat = os.stat(result.audio_path)
388
- assert file_stat.st_mode & 0o600 # At least owner read/write
389
-
390
- def test_disk_space_monitoring(self, audio_service, temp_base_dir):
391
- """Test disk space monitoring during processing."""
392
- import shutil
393
-
394
- # Get initial disk space
395
- initial_space = shutil.disk_usage(temp_base_dir)
396
-
397
- audio_upload = AudioUploadDto(
398
- filename="space_test.wav",
399
- content=b"space_test_data" * 1000, # Larger content
400
- content_type="audio/wav",
401
- size=len(b"space_test_data" * 1000)
402
- )
403
-
404
- request = ProcessingRequestDto(
405
- audio=audio_upload,
406
- asr_model="whisper-small",
407
- target_language="es",
408
- voice="chatterbox",
409
- speed=1.0,
410
- requires_translation=True
411
- )
412
-
413
- result = audio_service.process_audio_pipeline(request)
414
-
415
- assert result.success is True
416
-
417
- # Verify disk space hasn't been exhausted
418
- final_space = shutil.disk_usage(temp_base_dir)
419
- assert final_space.free > 0
420
-
421
- def test_file_naming_conventions(self, audio_service, temp_base_dir):
422
- """Test file naming conventions and uniqueness."""
423
- results = []
424
-
425
- # Process multiple files to test naming
426
- for i in range(3):
427
- audio_upload = AudioUploadDto(
428
- filename=f"naming_test_{i}.wav",
429
- content=f"naming_test_data_{i}".encode(),
430
- content_type="audio/wav",
431
- size=len(f"naming_test_data_{i}".encode())
432
- )
433
-
434
- request = ProcessingRequestDto(
435
- audio=audio_upload,
436
- asr_model="whisper-small",
437
- target_language="es",
438
- voice="chatterbox",
439
- speed=1.0,
440
- requires_translation=True
441
- )
442
-
443
- result = audio_service.process_audio_pipeline(request)
444
- results.append(result)
445
-
446
- # Verify all results are successful
447
- for result in results:
448
- assert result.success is True
449
- assert result.audio_path is not None
450
-
451
- # Verify unique file names
452
- output_paths = [r.audio_path for r in results]
453
- assert len(set(output_paths)) == len(output_paths) # All unique
454
-
455
- # Verify naming convention
456
- for path in output_paths:
457
- filename = os.path.basename(path)
458
- assert filename.startswith("output_")
459
- assert filename.endswith(".wav")
460
-
461
- def test_file_encoding_handling(self, audio_service, temp_base_dir):
462
- """Test handling of different file encodings and special characters."""
463
- # Test with filename containing special characters
464
- special_filename = "test_file_ñáéíóú_测试.wav"
465
-
466
- audio_upload = AudioUploadDto(
467
- filename=special_filename,
468
- content=b"encoding_test_data",
469
- content_type="audio/wav",
470
- size=len(b"encoding_test_data")
471
- )
472
-
473
- request = ProcessingRequestDto(
474
- audio=audio_upload,
475
- asr_model="whisper-small",
476
- target_language="es",
477
- voice="chatterbox",
478
- speed=1.0,
479
- requires_translation=True
480
- )
481
-
482
- result = audio_service.process_audio_pipeline(request)
483
-
484
- assert result.success is True
485
- assert result.audio_path is not None
486
- assert os.path.exists(result.audio_path)
487
-
488
- def test_file_cleanup_context_manager(self, mock_container, mock_config, temp_base_dir):
489
- """Test file cleanup using context manager."""
490
- initial_files = set(os.listdir(temp_base_dir))
491
-
492
- with AudioProcessingApplicationService(mock_container, mock_config) as service:
493
- audio_upload = AudioUploadDto(
494
- filename="context_test.wav",
495
- content=b"context_test_data",
496
- content_type="audio/wav",
497
- size=len(b"context_test_data")
498
- )
499
-
500
- request = ProcessingRequestDto(
501
- audio=audio_upload,
502
- asr_model="whisper-small",
503
- target_language="es",
504
- voice="chatterbox",
505
- speed=1.0,
506
- requires_translation=True
507
- )
508
-
509
- result = service.process_audio_pipeline(request)
510
- assert result.success is True
511
-
512
- # Verify cleanup occurred when exiting context
513
- final_files = set(os.listdir(temp_base_dir))
514
- new_files = final_files - initial_files
515
-
516
- # Should have minimal new files after context exit
517
- assert len(new_files) <= 1 # Possibly just log file
518
-
519
- def test_file_recovery_after_interruption(self, audio_service, temp_base_dir, mock_container):
520
- """Test file recovery mechanisms after processing interruption."""
521
- # Mock provider to simulate interruption
522
- mock_tts_provider = mock_container.get_tts_provider.return_value
523
- mock_tts_provider.synthesize.side_effect = KeyboardInterrupt("Simulated interruption")
524
-
525
- audio_upload = AudioUploadDto(
526
- filename="interruption_test.wav",
527
- content=b"interruption_test_data",
528
- content_type="audio/wav",
529
- size=len(b"interruption_test_data")
530
- )
531
-
532
- request = ProcessingRequestDto(
533
- audio=audio_upload,
534
- asr_model="whisper-small",
535
- target_language="es",
536
- voice="chatterbox",
537
- speed=1.0,
538
- requires_translation=True
539
- )
540
-
541
- # Process should handle interruption gracefully
542
- with pytest.raises(KeyboardInterrupt):
543
- audio_service.process_audio_pipeline(request)
544
-
545
- # Verify cleanup still occurred
546
- # (In real implementation, this would be handled by signal handlers)
547
-
548
- def test_file_metadata_preservation(self, audio_service, temp_base_dir):
549
- """Test preservation of file metadata during processing."""
550
- original_filename = "metadata_test.wav"
551
- original_content = b"metadata_test_data"
552
-
553
- audio_upload = AudioUploadDto(
554
- filename=original_filename,
555
- content=original_content,
556
- content_type="audio/wav",
557
- size=len(original_content)
558
- )
559
-
560
- request = ProcessingRequestDto(
561
- audio=audio_upload,
562
- asr_model="whisper-small",
563
- target_language="es",
564
- voice="chatterbox",
565
- speed=1.0,
566
- requires_translation=True
567
- )
568
-
569
- result = audio_service.process_audio_pipeline(request)
570
-
571
- assert result.success is True
572
- assert result.metadata is not None
573
-
574
- # Verify original filename is preserved in metadata
575
- correlation_id = result.metadata.get('correlation_id')
576
- assert correlation_id is not None
577
-
578
- # Verify output file exists
579
- assert result.audio_path is not None
580
- assert os.path.exists(result.audio_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/integration/test_performance_and_errors.py DELETED
@@ -1,550 +0,0 @@
1
- """Integration tests for performance and error scenario testing."""
2
-
3
- import time
4
- import pytest
5
- import threading
6
- import queue
7
- import psutil
8
- import os
9
- from unittest.mock import Mock, patch, MagicMock
10
- from typing import List, Dict, Any, Optional
11
-
12
- from src.application.services.audio_processing_service import AudioProcessingApplicationService
13
- from src.application.dtos.audio_upload_dto import AudioUploadDto
14
- from src.application.dtos.processing_request_dto import ProcessingRequestDto
15
- from src.application.dtos.processing_result_dto import ProcessingResultDto
16
- from src.infrastructure.config.dependency_container import DependencyContainer
17
- from src.infrastructure.config.app_config import AppConfig
18
- from src.domain.models.audio_content import AudioContent
19
- from src.domain.models.text_content import TextContent
20
- from src.domain.exceptions import (
21
- SpeechRecognitionException,
22
- TranslationFailedException,
23
- SpeechSynthesisException,
24
- AudioProcessingException,
25
- ProviderNotAvailableException
26
- )
27
-
28
-
29
- class TestPerformanceAndErrors:
30
- """Integration tests for performance and error scenarios."""
31
-
32
- @pytest.fixture
33
- def mock_config(self, tmp_path):
34
- """Create mock configuration for testing."""
35
- config = Mock(spec=AppConfig)
36
-
37
- # Processing configuration
38
- config.get_processing_config.return_value = {
39
- 'max_file_size_mb': 100,
40
- 'supported_audio_formats': ['wav', 'mp3', 'flac'],
41
- 'temp_dir': str(tmp_path),
42
- 'cleanup_temp_files': True,
43
- 'processing_timeout': 300, # 5 minutes
44
- 'max_concurrent_requests': 10
45
- }
46
-
47
- # Logging configuration
48
- config.get_logging_config.return_value = {
49
- 'level': 'INFO',
50
- 'enable_file_logging': False,
51
- 'log_file_path': str(tmp_path / 'test.log'),
52
- 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
53
- }
54
-
55
- # STT configuration
56
- config.get_stt_config.return_value = {
57
- 'preferred_providers': ['parakeet', 'whisper-small', 'whisper-medium'],
58
- 'provider_timeout': 60.0,
59
- 'max_retries': 2
60
- }
61
-
62
- # TTS configuration
63
- config.get_tts_config.return_value = {
64
- 'preferred_providers': ['chatterbox'],
65
- 'provider_timeout': 30.0,
66
- 'max_retries': 3
67
- }
68
-
69
- # Translation configuration
70
- config.get_translation_config.return_value = {
71
- 'provider_timeout': 45.0,
72
- 'max_retries': 2,
73
- 'chunk_size': 512
74
- }
75
-
76
- return config
77
-
78
- @pytest.fixture
79
- def mock_container(self, mock_config):
80
- """Create mock dependency container."""
81
- container = Mock(spec=DependencyContainer)
82
- container.resolve.return_value = mock_config
83
-
84
- # Mock providers with configurable behavior
85
- self._setup_mock_providers(container)
86
-
87
- return container
88
-
89
- def _setup_mock_providers(self, container):
90
- """Setup mock providers with configurable behavior."""
91
- # Mock STT provider
92
- mock_stt_provider = Mock()
93
- mock_stt_provider.transcribe.return_value = TextContent(
94
- text="Performance test transcription",
95
- language="en"
96
- )
97
- container.get_stt_provider.return_value = mock_stt_provider
98
-
99
- # Mock translation provider
100
- mock_translation_provider = Mock()
101
- mock_translation_provider.translate.return_value = TextContent(
102
- text="Transcripción de prueba de rendimiento",
103
- language="es"
104
- )
105
- container.get_translation_provider.return_value = mock_translation_provider
106
-
107
- # Mock TTS provider
108
- mock_tts_provider = Mock()
109
- mock_tts_provider.synthesize.return_value = AudioContent(
110
- data=b"performance_test_audio_data",
111
- format="wav",
112
- sample_rate=22050,
113
- duration=3.0
114
- )
115
- container.get_tts_provider.return_value = mock_tts_provider
116
-
117
- @pytest.fixture
118
- def audio_service(self, mock_container, mock_config):
119
- """Create audio processing service."""
120
- return AudioProcessingApplicationService(mock_container, mock_config)
121
-
122
- @pytest.fixture
123
- def sample_request(self):
124
- """Create sample processing request."""
125
- audio_upload = AudioUploadDto(
126
- filename="performance_test.wav",
127
- content=b"performance_test_audio_data",
128
- content_type="audio/wav",
129
- size=len(b"performance_test_audio_data")
130
- )
131
-
132
- return ProcessingRequestDto(
133
- audio=audio_upload,
134
- asr_model="whisper-small",
135
- target_language="es",
136
- voice="chatterbox",
137
- speed=1.0,
138
- requires_translation=True
139
- )
140
-
141
- def test_processing_time_performance(self, audio_service, sample_request):
142
- """Test processing time performance benchmarks."""
143
- # Warm up
144
- audio_service.process_audio_pipeline(sample_request)
145
-
146
- # Measure processing time
147
- start_time = time.time()
148
- result = audio_service.process_audio_pipeline(sample_request)
149
- end_time = time.time()
150
-
151
- processing_time = end_time - start_time
152
-
153
- assert result.success is True
154
- assert result.processing_time > 0
155
- assert result.processing_time <= processing_time + 0.1 # Allow small margin
156
-
157
- # Performance benchmark: should complete within reasonable time
158
- assert processing_time < 5.0 # Should complete within 5 seconds for mock providers
159
-
160
- def test_memory_usage_performance(self, audio_service, sample_request):
161
- """Test memory usage during processing."""
162
- process = psutil.Process(os.getpid())
163
-
164
- # Measure initial memory
165
- initial_memory = process.memory_info().rss
166
-
167
- # Process multiple requests
168
- for _ in range(10):
169
- result = audio_service.process_audio_pipeline(sample_request)
170
- assert result.success is True
171
-
172
- # Measure final memory
173
- final_memory = process.memory_info().rss
174
- memory_increase = final_memory - initial_memory
175
-
176
- # Memory increase should be reasonable (less than 100MB for test data)
177
- assert memory_increase < 100 * 1024 * 1024
178
-
179
- def test_concurrent_processing_performance(self, audio_service, sample_request):
180
- """Test performance under concurrent load."""
181
- num_threads = 5
182
- results_queue = queue.Queue()
183
-
184
- def process_request():
185
- try:
186
- start_time = time.time()
187
- result = audio_service.process_audio_pipeline(sample_request)
188
- end_time = time.time()
189
- results_queue.put((result, end_time - start_time))
190
- except Exception as e:
191
- results_queue.put(e)
192
-
193
- # Start concurrent processing
194
- threads = []
195
- start_time = time.time()
196
-
197
- for _ in range(num_threads):
198
- thread = threading.Thread(target=process_request)
199
- threads.append(thread)
200
- thread.start()
201
-
202
- # Wait for completion
203
- for thread in threads:
204
- thread.join()
205
-
206
- total_time = time.time() - start_time
207
-
208
- # Collect results
209
- results = []
210
- processing_times = []
211
-
212
- while not results_queue.empty():
213
- item = results_queue.get()
214
- if isinstance(item, Exception):
215
- pytest.fail(f"Concurrent processing failed: {item}")
216
- result, proc_time = item
217
- results.append(result)
218
- processing_times.append(proc_time)
219
-
220
- # Verify all succeeded
221
- assert len(results) == num_threads
222
- for result in results:
223
- assert result.success is True
224
-
225
- # Performance checks
226
- avg_processing_time = sum(processing_times) / len(processing_times)
227
- assert avg_processing_time < 10.0 # Average should be reasonable
228
- assert total_time < 15.0 # Total concurrent time should be reasonable
229
-
230
- def test_large_file_performance(self, audio_service):
231
- """Test performance with large audio files."""
232
- # Create large audio file (10MB)
233
- large_content = b"x" * (10 * 1024 * 1024)
234
-
235
- audio_upload = AudioUploadDto(
236
- filename="large_performance_test.wav",
237
- content=large_content,
238
- content_type="audio/wav",
239
- size=len(large_content)
240
- )
241
-
242
- request = ProcessingRequestDto(
243
- audio=audio_upload,
244
- asr_model="whisper-small",
245
- target_language="es",
246
- voice="chatterbox",
247
- speed=1.0,
248
- requires_translation=True
249
- )
250
-
251
- start_time = time.time()
252
- result = audio_service.process_audio_pipeline(request)
253
- end_time = time.time()
254
-
255
- processing_time = end_time - start_time
256
-
257
- assert result.success is True
258
- # Large files should still complete within reasonable time
259
- assert processing_time < 30.0
260
-
261
- def test_stt_provider_failure_recovery(self, audio_service, sample_request, mock_container):
262
- """Test recovery from STT provider failures."""
263
- mock_stt_provider = mock_container.get_stt_provider.return_value
264
-
265
- # Mock first call to fail, second to succeed
266
- mock_stt_provider.transcribe.side_effect = [
267
- SpeechRecognitionException("STT provider temporarily unavailable"),
268
- TextContent(text="Recovered transcription", language="en")
269
- ]
270
-
271
- result = audio_service.process_audio_pipeline(sample_request)
272
-
273
- assert result.success is True
274
- assert "Recovered transcription" in result.original_text
275
-
276
- def test_translation_provider_failure_recovery(self, audio_service, sample_request, mock_container):
277
- """Test recovery from translation provider failures."""
278
- mock_translation_provider = mock_container.get_translation_provider.return_value
279
-
280
- # Mock first call to fail, second to succeed
281
- mock_translation_provider.translate.side_effect = [
282
- TranslationFailedException("Translation service temporarily unavailable"),
283
- TextContent(text="Traducción recuperada", language="es")
284
- ]
285
-
286
- result = audio_service.process_audio_pipeline(sample_request)
287
-
288
- assert result.success is True
289
- assert "Traducción recuperada" in result.translated_text
290
-
291
- def test_tts_provider_failure_recovery(self, audio_service, sample_request, mock_container):
292
- """Test recovery from TTS provider failures."""
293
- mock_tts_provider = mock_container.get_tts_provider.return_value
294
-
295
- # Mock first call to fail, second to succeed
296
- mock_tts_provider.synthesize.side_effect = [
297
- SpeechSynthesisException("TTS provider temporarily unavailable"),
298
- AudioContent(
299
- data=b"recovered_audio_data",
300
- format="wav",
301
- sample_rate=22050,
302
- duration=2.5
303
- )
304
- ]
305
-
306
- result = audio_service.process_audio_pipeline(sample_request)
307
-
308
- assert result.success is True
309
- assert result.audio_path is not None
310
-
311
- def test_multiple_provider_failures(self, audio_service, sample_request, mock_container):
312
- """Test handling of multiple provider failures."""
313
- # Mock all providers to fail initially
314
- mock_stt_provider = mock_container.get_stt_provider.return_value
315
- mock_translation_provider = mock_container.get_translation_provider.return_value
316
- mock_tts_provider = mock_container.get_tts_provider.return_value
317
-
318
- mock_stt_provider.transcribe.side_effect = SpeechRecognitionException("STT failed")
319
- mock_translation_provider.translate.side_effect = TranslationFailedException("Translation failed")
320
- mock_tts_provider.synthesize.side_effect = SpeechSynthesisException("TTS failed")
321
-
322
- result = audio_service.process_audio_pipeline(sample_request)
323
-
324
- assert result.success is False
325
- assert result.error_message is not None
326
- assert result.error_code is not None
327
-
328
- def test_timeout_handling(self, audio_service, sample_request, mock_container):
329
- """Test handling of provider timeouts."""
330
- mock_stt_provider = mock_container.get_stt_provider.return_value
331
-
332
- def slow_transcribe(*args, **kwargs):
333
- time.sleep(2.0) # Simulate slow processing
334
- return TextContent(text="Slow transcription", language="en")
335
-
336
- mock_stt_provider.transcribe.side_effect = slow_transcribe
337
-
338
- start_time = time.time()
339
- result = audio_service.process_audio_pipeline(sample_request)
340
- end_time = time.time()
341
-
342
- processing_time = end_time - start_time
343
-
344
- # Should complete despite slow provider
345
- assert result.success is True
346
- assert processing_time >= 2.0 # Should include the delay
347
-
348
- def test_invalid_input_handling(self, audio_service):
349
- """Test handling of invalid input data."""
350
- # Test with invalid audio format
351
- invalid_audio = AudioUploadDto(
352
- filename="invalid.xyz",
353
- content=b"invalid_audio_data",
354
- content_type="audio/xyz",
355
- size=len(b"invalid_audio_data")
356
- )
357
-
358
- request = ProcessingRequestDto(
359
- audio=invalid_audio,
360
- asr_model="whisper-small",
361
- target_language="es",
362
- voice="chatterbox",
363
- speed=1.0,
364
- requires_translation=True
365
- )
366
-
367
- result = audio_service.process_audio_pipeline(request)
368
-
369
- assert result.success is False
370
- assert result.error_code is not None
371
- assert "format" in result.error_message.lower() or "unsupported" in result.error_message.lower()
372
-
373
- def test_oversized_file_handling(self, audio_service, mock_config):
374
- """Test handling of oversized files."""
375
- # Mock config to have small file size limit
376
- mock_config.get_processing_config.return_value['max_file_size_mb'] = 1
377
-
378
- # Create file larger than limit
379
- large_content = b"x" * (2 * 1024 * 1024) # 2MB
380
-
381
- oversized_audio = AudioUploadDto(
382
- filename="oversized.wav",
383
- content=large_content,
384
- content_type="audio/wav",
385
- size=len(large_content)
386
- )
387
-
388
- request = ProcessingRequestDto(
389
- audio=oversized_audio,
390
- asr_model="whisper-small",
391
- target_language="es",
392
- voice="chatterbox",
393
- speed=1.0,
394
- requires_translation=True
395
- )
396
-
397
- result = audio_service.process_audio_pipeline(request)
398
-
399
- assert result.success is False
400
- assert result.error_code is not None
401
- assert "size" in result.error_message.lower() or "large" in result.error_message.lower()
402
-
403
- def test_corrupted_audio_handling(self, audio_service):
404
- """Test handling of corrupted audio data."""
405
- corrupted_audio = AudioUploadDto(
406
- filename="corrupted.wav",
407
- content=b"corrupted_data_not_audio",
408
- content_type="audio/wav",
409
- size=len(b"corrupted_data_not_audio")
410
- )
411
-
412
- request = ProcessingRequestDto(
413
- audio=corrupted_audio,
414
- asr_model="whisper-small",
415
- target_language="es",
416
- voice="chatterbox",
417
- speed=1.0,
418
- requires_translation=True
419
- )
420
-
421
- result = audio_service.process_audio_pipeline(request)
422
-
423
- # Should handle gracefully (success depends on implementation)
424
- assert result.error_message is None or "audio" in result.error_message.lower()
425
-
426
- def test_network_error_simulation(self, audio_service, sample_request, mock_container):
427
- """Test handling of network-related errors."""
428
- mock_translation_provider = mock_container.get_translation_provider.return_value
429
-
430
- # Simulate network errors
431
- mock_translation_provider.translate.side_effect = [
432
- ConnectionError("Network connection failed"),
433
- TimeoutError("Request timed out"),
434
- TextContent(text="Network recovered translation", language="es")
435
- ]
436
-
437
- result = audio_service.process_audio_pipeline(sample_request)
438
-
439
- # Should recover from network errors
440
- assert result.success is True
441
- assert "Network recovered translation" in result.translated_text
442
-
443
- def test_resource_exhaustion_handling(self, audio_service, sample_request):
444
- """Test handling of resource exhaustion scenarios."""
445
- # Simulate memory pressure by processing many requests
446
- results = []
447
-
448
- for i in range(20): # Process many requests
449
- result = audio_service.process_audio_pipeline(sample_request)
450
- results.append(result)
451
-
452
- # All should succeed despite resource pressure
453
- assert result.success is True
454
-
455
- # Verify all completed successfully
456
- assert len(results) == 20
457
- for result in results:
458
- assert result.success is True
459
-
460
- def test_error_correlation_tracking(self, audio_service, sample_request, mock_container):
461
- """Test error correlation tracking across pipeline stages."""
462
- mock_stt_provider = mock_container.get_stt_provider.return_value
463
- mock_stt_provider.transcribe.side_effect = SpeechRecognitionException("STT correlation test error")
464
-
465
- result = audio_service.process_audio_pipeline(sample_request)
466
-
467
- assert result.success is False
468
- assert result.metadata is not None
469
- assert 'correlation_id' in result.metadata
470
-
471
- # Verify correlation ID is consistent
472
- correlation_id = result.metadata['correlation_id']
473
- assert isinstance(correlation_id, str)
474
- assert len(correlation_id) > 0
475
-
476
- def test_graceful_degradation(self, audio_service, sample_request, mock_container):
477
- """Test graceful degradation when some features fail."""
478
- # Mock translation to fail but allow STT and TTS to succeed
479
- mock_translation_provider = mock_container.get_translation_provider.return_value
480
- mock_translation_provider.translate.side_effect = TranslationFailedException("Translation unavailable")
481
-
482
- # Modify request to not require translation
483
- sample_request.requires_translation = False
484
- sample_request.target_language = "en" # Same as source
485
-
486
- result = audio_service.process_audio_pipeline(sample_request)
487
-
488
- # Should succeed without translation
489
- assert result.success is True
490
- assert result.translated_text is None # No translation performed
491
-
492
- def test_circuit_breaker_behavior(self, audio_service, sample_request, mock_container):
493
- """Test circuit breaker behavior under repeated failures."""
494
- mock_tts_provider = mock_container.get_tts_provider.return_value
495
-
496
- # Mock repeated failures to trigger circuit breaker
497
- mock_tts_provider.synthesize.side_effect = SpeechSynthesisException("Repeated TTS failure")
498
-
499
- results = []
500
- for _ in range(5): # Multiple attempts
501
- result = audio_service.process_audio_pipeline(sample_request)
502
- results.append(result)
503
-
504
- # All should fail, but circuit breaker should prevent excessive retries
505
- for result in results:
506
- assert result.success is False
507
- assert result.error_code is not None
508
-
509
- def test_performance_metrics_collection(self, audio_service, sample_request):
510
- """Test collection of performance metrics."""
511
- result = audio_service.process_audio_pipeline(sample_request)
512
-
513
- assert result.success is True
514
- assert result.processing_time > 0
515
- assert result.metadata is not None
516
-
517
- # Verify performance-related metadata
518
- metadata = result.metadata
519
- assert 'correlation_id' in metadata
520
- assert 'asr_model' in metadata
521
- assert 'target_language' in metadata
522
- assert 'voice' in metadata
523
-
524
- def test_stress_testing(self, audio_service, sample_request):
525
- """Test system behavior under stress conditions."""
526
- num_requests = 50
527
- results = []
528
-
529
- start_time = time.time()
530
-
531
- for i in range(num_requests):
532
- result = audio_service.process_audio_pipeline(sample_request)
533
- results.append(result)
534
-
535
- end_time = time.time()
536
- total_time = end_time - start_time
537
-
538
- # Verify all requests completed
539
- assert len(results) == num_requests
540
-
541
- # Calculate success rate
542
- successful_results = [r for r in results if r.success]
543
- success_rate = len(successful_results) / len(results)
544
-
545
- # Should maintain high success rate under stress
546
- assert success_rate >= 0.95 # At least 95% success rate
547
-
548
- # Performance should remain reasonable
549
- avg_time_per_request = total_time / num_requests
550
- assert avg_time_per_request < 1.0 # Average less than 1 second per request
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/integration/test_provider_integration.py DELETED
@@ -1,462 +0,0 @@
1
- """Integration tests for provider integration and switching."""
2
-
3
- import pytest
4
- from unittest.mock import Mock, patch, MagicMock
5
- from typing import Dict, Any, List
6
-
7
- from src.infrastructure.config.dependency_container import DependencyContainer
8
- from src.infrastructure.config.app_config import AppConfig
9
- from src.infrastructure.tts.provider_factory import TTSProviderFactory
10
- from src.infrastructure.stt.provider_factory import STTProviderFactory
11
- from src.infrastructure.translation.provider_factory import TranslationProviderFactory
12
- from src.domain.models.audio_content import AudioContent
13
- from src.domain.models.text_content import TextContent
14
- from src.domain.models.speech_synthesis_request import SpeechSynthesisRequest
15
- from src.domain.models.translation_request import TranslationRequest
16
- from src.domain.models.voice_settings import VoiceSettings
17
- from src.domain.exceptions import (
18
- SpeechRecognitionException,
19
- TranslationFailedException,
20
- SpeechSynthesisException,
21
- ProviderNotAvailableException
22
- )
23
-
24
-
25
- class TestProviderIntegration:
26
- """Integration tests for provider integration and switching."""
27
-
28
- @pytest.fixture
29
- def mock_config(self):
30
- """Create mock configuration for testing."""
31
- config = Mock(spec=AppConfig)
32
-
33
- # TTS configuration
34
- config.tts.preferred_providers = ['chatterbox']
35
- config.tts.fallback_enabled = True
36
- config.tts.provider_timeout = 30.0
37
-
38
- # STT configuration
39
- config.stt.default_model = 'whisper-small'
40
- config.stt.fallback_models = ['whisper-medium', 'parakeet']
41
- config.stt.provider_timeout = 60.0
42
-
43
- # Translation configuration
44
- config.translation.default_provider = 'nllb'
45
- config.translation.fallback_enabled = True
46
- config.translation.chunk_size = 512
47
-
48
- return config
49
-
50
- @pytest.fixture
51
- def dependency_container(self, mock_config):
52
- """Create dependency container with mock configuration."""
53
- container = DependencyContainer(mock_config)
54
- return container
55
-
56
- @pytest.fixture
57
- def sample_audio_content(self):
58
- """Create sample audio content for testing."""
59
- return AudioContent(
60
- data=b"fake_audio_data",
61
- format="wav",
62
- sample_rate=16000,
63
- duration=2.5
64
- )
65
-
66
- @pytest.fixture
67
- def sample_text_content(self):
68
- """Create sample text content for testing."""
69
- return TextContent(
70
- text="Hello, this is a test message.",
71
- language="en"
72
- )
73
-
74
- def test_tts_provider_switching(self, dependency_container, sample_text_content):
75
- """Test switching between different TTS providers."""
76
- voice_settings = VoiceSettings(
77
- voice_id="test_voice",
78
- speed=1.0,
79
- language="en"
80
- )
81
-
82
- synthesis_request = SpeechSynthesisRequest(
83
- text=sample_text_content.text,
84
- voice_settings=voice_settings
85
- )
86
-
87
- # Test each TTS provider
88
- providers_to_test = ['chatterbox']
89
-
90
- for provider_name in providers_to_test:
91
- with patch(f'src.infrastructure.tts.{provider_name}_provider') as mock_provider_module:
92
- # Mock the provider class
93
- mock_provider_class = Mock()
94
- mock_provider_instance = Mock()
95
- mock_provider_instance.synthesize.return_value = AudioContent(
96
- data=f"{provider_name}_audio_data".encode(),
97
- format="wav",
98
- sample_rate=22050,
99
- duration=2.0
100
- )
101
- mock_provider_class.return_value = mock_provider_instance
102
- setattr(mock_provider_module, f'{provider_name.title()}Provider', mock_provider_class)
103
-
104
- # Get provider from container
105
- provider = dependency_container.get_tts_provider(provider_name)
106
-
107
- # Test synthesis
108
- result = provider.synthesize(synthesis_request)
109
-
110
- assert isinstance(result, AudioContent)
111
- assert provider_name.encode() in result.data
112
- mock_provider_instance.synthesize.assert_called_once()
113
-
114
- def test_tts_provider_fallback(self, dependency_container, sample_text_content):
115
- """Test TTS provider fallback mechanism."""
116
- voice_settings = VoiceSettings(
117
- voice_id="test_voice",
118
- speed=1.0,
119
- language="en"
120
- )
121
-
122
- synthesis_request = SpeechSynthesisRequest(
123
- text=sample_text_content.text,
124
- voice_settings=voice_settings
125
- )
126
-
127
- with patch('src.infrastructure.tts.provider_factory.TTSProviderFactory') as mock_factory_class:
128
- mock_factory = Mock()
129
- mock_factory_class.return_value = mock_factory
130
-
131
- # Mock first provider to fail, second to succeed
132
- mock_provider1 = Mock()
133
- mock_provider1.synthesize.side_effect = SpeechSynthesisException("Provider 1 failed")
134
-
135
- mock_provider2 = Mock()
136
- mock_provider2.synthesize.return_value = AudioContent(
137
- data=b"fallback_audio_data",
138
- format="wav",
139
- sample_rate=22050,
140
- duration=2.0
141
- )
142
-
143
- mock_factory.get_provider_with_fallback.return_value = mock_provider2
144
-
145
- # Get provider with fallback
146
- provider = dependency_container.get_tts_provider()
147
- result = provider.synthesize(synthesis_request)
148
-
149
- assert isinstance(result, AudioContent)
150
- assert b"fallback_audio_data" in result.data
151
-
152
- def test_stt_provider_switching(self, dependency_container, sample_audio_content):
153
- """Test switching between different STT providers."""
154
- providers_to_test = ['whisper-small', 'whisper-medium', 'parakeet']
155
-
156
- for provider_name in providers_to_test:
157
- with patch('src.infrastructure.stt.provider_factory.STTProviderFactory') as mock_factory_class:
158
- mock_factory = Mock()
159
- mock_factory_class.return_value = mock_factory
160
-
161
- mock_provider = Mock()
162
- mock_provider.transcribe.return_value = TextContent(
163
- text=f"Transcription from {provider_name}",
164
- language="en"
165
- )
166
- mock_factory.create_provider.return_value = mock_provider
167
-
168
- # Get provider from container
169
- provider = dependency_container.get_stt_provider(provider_name)
170
-
171
- # Test transcription
172
- result = provider.transcribe(sample_audio_content, provider_name)
173
-
174
- assert isinstance(result, TextContent)
175
- assert provider_name in result.text
176
- mock_provider.transcribe.assert_called_once()
177
-
178
- def test_stt_provider_fallback(self, dependency_container, sample_audio_content):
179
- """Test STT provider fallback mechanism."""
180
- with patch('src.infrastructure.stt.provider_factory.STTProviderFactory') as mock_factory_class:
181
- mock_factory = Mock()
182
- mock_factory_class.return_value = mock_factory
183
-
184
- # Mock first provider to fail, fallback to succeed
185
- mock_provider1 = Mock()
186
- mock_provider1.transcribe.side_effect = SpeechRecognitionException("Provider 1 failed")
187
-
188
- mock_provider2 = Mock()
189
- mock_provider2.transcribe.return_value = TextContent(
190
- text="Fallback transcription successful",
191
- language="en"
192
- )
193
-
194
- mock_factory.create_provider_with_fallback.return_value = mock_provider2
195
-
196
- # Get provider with fallback
197
- provider = dependency_container.get_stt_provider()
198
- result = provider.transcribe(sample_audio_content, "whisper-small")
199
-
200
- assert isinstance(result, TextContent)
201
- assert "Fallback transcription successful" in result.text
202
-
203
- def test_translation_provider_integration(self, dependency_container):
204
- """Test translation provider integration."""
205
- translation_request = TranslationRequest(
206
- text="Hello, how are you?",
207
- source_language="en",
208
- target_language="es"
209
- )
210
-
211
- with patch('src.infrastructure.translation.provider_factory.TranslationProviderFactory') as mock_factory_class:
212
- mock_factory = Mock()
213
- mock_factory_class.return_value = mock_factory
214
-
215
- mock_provider = Mock()
216
- mock_provider.translate.return_value = TextContent(
217
- text="Hola, ¿cómo estás?",
218
- language="es"
219
- )
220
- mock_factory.get_default_provider.return_value = mock_provider
221
-
222
- # Get translation provider
223
- provider = dependency_container.get_translation_provider()
224
- result = provider.translate(translation_request)
225
-
226
- assert isinstance(result, TextContent)
227
- assert result.text == "Hola, ¿cómo estás?"
228
- assert result.language == "es"
229
-
230
- def test_provider_availability_checking(self, dependency_container):
231
- """Test provider availability checking."""
232
- with patch('src.infrastructure.tts.provider_factory.TTSProviderFactory') as mock_factory_class:
233
- mock_factory = Mock()
234
- mock_factory_class.return_value = mock_factory
235
-
236
- # Mock availability checking
237
- mock_factory.is_provider_available.side_effect = lambda name: name in ['kokoro', 'dummy']
238
- mock_factory.get_available_providers.return_value = ['kokoro', 'dummy']
239
-
240
- # Test availability
241
- available_providers = mock_factory.get_available_providers()
242
-
243
- assert 'kokoro' in available_providers
244
- assert 'dummy' in available_providers
245
- assert 'dia' not in available_providers # Not available in mock
246
-
247
- def test_provider_configuration_loading(self, dependency_container, mock_config):
248
- """Test provider configuration loading and validation."""
249
- # Test TTS configuration
250
- tts_provider = dependency_container.get_tts_provider('chatterbox')
251
- assert tts_provider is not None
252
-
253
- # Test STT configuration
254
- stt_provider = dependency_container.get_stt_provider('whisper-small')
255
- assert stt_provider is not None
256
-
257
- # Test translation configuration
258
- translation_provider = dependency_container.get_translation_provider()
259
- assert translation_provider is not None
260
-
261
- def test_provider_error_handling(self, dependency_container, sample_audio_content):
262
- """Test provider error handling and recovery."""
263
- with patch('src.infrastructure.stt.provider_factory.STTProviderFactory') as mock_factory_class:
264
- mock_factory = Mock()
265
- mock_factory_class.return_value = mock_factory
266
-
267
- # Mock provider that always fails
268
- mock_provider = Mock()
269
- mock_provider.transcribe.side_effect = SpeechRecognitionException("Provider unavailable")
270
- mock_factory.create_provider.return_value = mock_provider
271
-
272
- # Test error handling
273
- provider = dependency_container.get_stt_provider('whisper-small')
274
-
275
- with pytest.raises(SpeechRecognitionException):
276
- provider.transcribe(sample_audio_content, 'whisper-small')
277
-
278
- def test_provider_performance_monitoring(self, dependency_container, sample_text_content):
279
- """Test provider performance monitoring."""
280
- import time
281
-
282
- voice_settings = VoiceSettings(
283
- voice_id="test_voice",
284
- speed=1.0,
285
- language="en"
286
- )
287
-
288
- synthesis_request = SpeechSynthesisRequest(
289
- text=sample_text_content.text,
290
- voice_settings=voice_settings
291
- )
292
-
293
- with patch('src.infrastructure.tts.provider_factory.TTSProviderFactory') as mock_factory_class:
294
- mock_factory = Mock()
295
- mock_factory_class.return_value = mock_factory
296
-
297
- mock_provider = Mock()
298
-
299
- def slow_synthesize(request):
300
- time.sleep(0.1) # Simulate processing time
301
- return AudioContent(
302
- data=b"slow_audio_data",
303
- format="wav",
304
- sample_rate=22050,
305
- duration=2.0
306
- )
307
-
308
- mock_provider.synthesize.side_effect = slow_synthesize
309
- mock_factory.create_provider.return_value = mock_provider
310
-
311
- # Measure performance
312
- start_time = time.time()
313
- provider = dependency_container.get_tts_provider('chatterbox')
314
- result = provider.synthesize(synthesis_request)
315
- end_time = time.time()
316
-
317
- processing_time = end_time - start_time
318
-
319
- assert isinstance(result, AudioContent)
320
- assert processing_time >= 0.1 # Should take at least the sleep time
321
-
322
- def test_provider_resource_cleanup(self, dependency_container):
323
- """Test provider resource cleanup."""
324
- # Get multiple providers
325
- tts_provider = dependency_container.get_tts_provider('chatterbox')
326
- stt_provider = dependency_container.get_stt_provider('whisper-small')
327
- translation_provider = dependency_container.get_translation_provider()
328
-
329
- assert tts_provider is not None
330
- assert stt_provider is not None
331
- assert translation_provider is not None
332
-
333
- # Test cleanup
334
- dependency_container.cleanup()
335
-
336
- # Verify cleanup was called (would need to mock the actual providers)
337
- # This is more of a smoke test to ensure cleanup doesn't crash
338
-
339
- def test_provider_concurrent_access(self, dependency_container, sample_text_content):
340
- """Test concurrent access to providers."""
341
- import threading
342
- import queue
343
-
344
- voice_settings = VoiceSettings(
345
- voice_id="test_voice",
346
- speed=1.0,
347
- language="en"
348
- )
349
-
350
- synthesis_request = SpeechSynthesisRequest(
351
- text=sample_text_content.text,
352
- voice_settings=voice_settings
353
- )
354
-
355
- results_queue = queue.Queue()
356
-
357
- def synthesize_audio():
358
- try:
359
- provider = dependency_container.get_tts_provider('chatterbox')
360
- with patch.object(provider, 'synthesize') as mock_synthesize:
361
- mock_synthesize.return_value = AudioContent(
362
- data=b"concurrent_audio_data",
363
- format="wav",
364
- sample_rate=22050,
365
- duration=2.0
366
- )
367
- result = provider.synthesize(synthesis_request)
368
- results_queue.put(result)
369
- except Exception as e:
370
- results_queue.put(e)
371
-
372
- # Start multiple threads
373
- threads = []
374
- for _ in range(3):
375
- thread = threading.Thread(target=synthesize_audio)
376
- threads.append(thread)
377
- thread.start()
378
-
379
- # Wait for completion
380
- for thread in threads:
381
- thread.join()
382
-
383
- # Verify results
384
- results = []
385
- while not results_queue.empty():
386
- result = results_queue.get()
387
- if isinstance(result, Exception):
388
- pytest.fail(f"Concurrent access failed: {result}")
389
- results.append(result)
390
-
391
- assert len(results) == 3
392
- for result in results:
393
- assert isinstance(result, AudioContent)
394
-
395
- def test_provider_configuration_updates(self, dependency_container, mock_config):
396
- """Test dynamic provider configuration updates."""
397
- # Initial configuration
398
- initial_providers = mock_config.tts.preferred_providers
399
- assert 'chatterbox' in initial_providers
400
-
401
- # Update configuration
402
- mock_config.tts.preferred_providers = ['chatterbox']
403
-
404
- # Verify configuration update affects provider selection
405
- # (This would require actual implementation of dynamic config updates)
406
- updated_providers = mock_config.tts.preferred_providers
407
- assert 'chatterbox' in updated_providers
408
- assert 'dummy' in updated_providers
409
-
410
- def test_provider_health_checking(self, dependency_container):
411
- """Test provider health checking mechanisms."""
412
- with patch('src.infrastructure.tts.provider_factory.TTSProviderFactory') as mock_factory_class:
413
- mock_factory = Mock()
414
- mock_factory_class.return_value = mock_factory
415
-
416
- # Mock health check methods
417
- mock_factory.check_provider_health.return_value = {
418
- 'kokoro': {'status': 'healthy', 'response_time': 0.1},
419
- 'dia': {'status': 'unhealthy', 'error': 'Connection timeout'},
420
- 'dummy': {'status': 'healthy', 'response_time': 0.05}
421
- }
422
-
423
- health_status = mock_factory.check_provider_health()
424
-
425
- assert health_status['kokoro']['status'] == 'healthy'
426
- assert health_status['dia']['status'] == 'unhealthy'
427
- assert health_status['dummy']['status'] == 'healthy'
428
-
429
- def test_provider_load_balancing(self, dependency_container):
430
- """Test provider load balancing mechanisms."""
431
- with patch('src.infrastructure.tts.provider_factory.TTSProviderFactory') as mock_factory_class:
432
- mock_factory = Mock()
433
- mock_factory_class.return_value = mock_factory
434
-
435
- # Mock load balancing
436
- provider_calls = {'kokoro': 0, 'dia': 0, 'dummy': 0}
437
-
438
- def mock_get_provider(name=None):
439
- if name is None:
440
- # Round-robin selection
441
- providers = ['kokoro', 'dia', 'dummy']
442
- selected = min(providers, key=lambda p: provider_calls[p])
443
- provider_calls[selected] += 1
444
- name = selected
445
-
446
- mock_provider = Mock()
447
- mock_provider.name = name
448
- return mock_provider
449
-
450
- mock_factory.create_provider.side_effect = mock_get_provider
451
-
452
- # Get multiple providers to test load balancing
453
- providers = []
454
- for _ in range(6):
455
- provider = mock_factory.create_provider()
456
- providers.append(provider)
457
-
458
- # Verify load distribution
459
- provider_names = [p.name for p in providers]
460
- assert provider_names.count('kokoro') == 2
461
- assert provider_names.count('dia') == 2
462
- assert provider_names.count('dummy') == 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/application/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Unit tests for application layer"""
 
 
tests/unit/application/dtos/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Unit tests for application DTOs"""
 
 
tests/unit/application/dtos/test_audio_upload_dto.py DELETED
@@ -1,245 +0,0 @@
1
- """Unit tests for AudioUploadDto"""
2
-
3
- import pytest
4
- import os
5
-
6
- from src.application.dtos.audio_upload_dto import AudioUploadDto
7
-
8
-
9
- class TestAudioUploadDto:
10
- """Test cases for AudioUploadDto"""
11
-
12
- def test_valid_audio_upload_dto(self):
13
- """Test creating a valid AudioUploadDto"""
14
- filename = "test_audio.wav"
15
- content = b"fake_audio_content_" + b"x" * 1000 # 1KB+ of fake audio
16
- content_type = "audio/wav"
17
-
18
- dto = AudioUploadDto(
19
- filename=filename,
20
- content=content,
21
- content_type=content_type
22
- )
23
-
24
- assert dto.filename == filename
25
- assert dto.content == content
26
- assert dto.content_type == content_type
27
- assert dto.size == len(content)
28
-
29
- def test_audio_upload_dto_with_explicit_size(self):
30
- """Test creating AudioUploadDto with explicit size"""
31
- filename = "test_audio.mp3"
32
- content = b"fake_audio_content_" + b"x" * 2000
33
- content_type = "audio/mpeg"
34
- size = 2500
35
-
36
- dto = AudioUploadDto(
37
- filename=filename,
38
- content=content,
39
- content_type=content_type,
40
- size=size
41
- )
42
-
43
- assert dto.size == size # Should use explicit size, not calculated
44
-
45
- def test_empty_filename_validation(self):
46
- """Test validation with empty filename"""
47
- with pytest.raises(ValueError, match="Filename cannot be empty"):
48
- AudioUploadDto(
49
- filename="",
50
- content=b"fake_audio_content_" + b"x" * 1000,
51
- content_type="audio/wav"
52
- )
53
-
54
- def test_empty_content_validation(self):
55
- """Test validation with empty content"""
56
- with pytest.raises(ValueError, match="Audio content cannot be empty"):
57
- AudioUploadDto(
58
- filename="test.wav",
59
- content=b"",
60
- content_type="audio/wav"
61
- )
62
-
63
- def test_empty_content_type_validation(self):
64
- """Test validation with empty content type"""
65
- with pytest.raises(ValueError, match="Content type cannot be empty"):
66
- AudioUploadDto(
67
- filename="test.wav",
68
- content=b"fake_audio_content_" + b"x" * 1000,
69
- content_type=""
70
- )
71
-
72
- def test_unsupported_file_extension_validation(self):
73
- """Test validation with unsupported file extension"""
74
- with pytest.raises(ValueError, match="Unsupported file extension"):
75
- AudioUploadDto(
76
- filename="test.xyz",
77
- content=b"fake_audio_content_" + b"x" * 1000,
78
- content_type="audio/wav"
79
- )
80
-
81
- def test_supported_file_extensions(self):
82
- """Test all supported file extensions"""
83
- supported_extensions = ['.wav', '.mp3', '.m4a', '.flac', '.ogg']
84
- content = b"fake_audio_content_" + b"x" * 1000
85
-
86
- for ext in supported_extensions:
87
- filename = f"test{ext}"
88
- content_type = f"audio/{ext[1:]}" if ext != '.m4a' else "audio/mp4"
89
-
90
- # Should not raise exception
91
- dto = AudioUploadDto(
92
- filename=filename,
93
- content=content,
94
- content_type=content_type
95
- )
96
- assert dto.file_extension == ext
97
-
98
- def test_case_insensitive_extension_validation(self):
99
- """Test case insensitive extension validation"""
100
- content = b"fake_audio_content_" + b"x" * 1000
101
-
102
- # Should work with uppercase extension
103
- dto = AudioUploadDto(
104
- filename="test.WAV",
105
- content=content,
106
- content_type="audio/wav"
107
- )
108
- assert dto.file_extension == ".wav" # Should be normalized to lowercase
109
-
110
- def test_file_too_large_validation(self):
111
- """Test validation with file too large"""
112
- max_size = 100 * 1024 * 1024 # 100MB
113
- large_content = b"x" * (max_size + 1)
114
-
115
- with pytest.raises(ValueError, match="File too large"):
116
- AudioUploadDto(
117
- filename="test.wav",
118
- content=large_content,
119
- content_type="audio/wav"
120
- )
121
-
122
- def test_file_too_small_validation(self):
123
- """Test validation with file too small"""
124
- small_content = b"x" * 500 # Less than 1KB
125
-
126
- with pytest.raises(ValueError, match="File too small"):
127
- AudioUploadDto(
128
- filename="test.wav",
129
- content=small_content,
130
- content_type="audio/wav"
131
- )
132
-
133
- def test_invalid_content_type_validation(self):
134
- """Test validation with invalid content type"""
135
- content = b"fake_audio_content_" + b"x" * 1000
136
-
137
- with pytest.raises(ValueError, match="Invalid content type"):
138
- AudioUploadDto(
139
- filename="test.wav",
140
- content=content,
141
- content_type="text/plain" # Not audio/*
142
- )
143
-
144
- def test_file_extension_property(self):
145
- """Test file_extension property"""
146
- dto = AudioUploadDto(
147
- filename="test_audio.MP3",
148
- content=b"fake_audio_content_" + b"x" * 1000,
149
- content_type="audio/mpeg"
150
- )
151
-
152
- assert dto.file_extension == ".mp3"
153
-
154
- def test_base_filename_property(self):
155
- """Test base_filename property"""
156
- dto = AudioUploadDto(
157
- filename="test_audio.wav",
158
- content=b"fake_audio_content_" + b"x" * 1000,
159
- content_type="audio/wav"
160
- )
161
-
162
- assert dto.base_filename == "test_audio"
163
-
164
- def test_to_dict_method(self):
165
- """Test to_dict method"""
166
- filename = "test_audio.wav"
167
- content = b"fake_audio_content_" + b"x" * 1000
168
- content_type = "audio/wav"
169
-
170
- dto = AudioUploadDto(
171
- filename=filename,
172
- content=content,
173
- content_type=content_type
174
- )
175
-
176
- result = dto.to_dict()
177
-
178
- assert result['filename'] == filename
179
- assert result['content_type'] == content_type
180
- assert result['size'] == len(content)
181
- assert result['file_extension'] == ".wav"
182
-
183
- # Should not include content in dict representation
184
- assert 'content' not in result
185
-
186
- def test_size_calculation_on_init(self):
187
- """Test that size is calculated automatically if not provided"""
188
- content = b"fake_audio_content_" + b"x" * 1500
189
-
190
- dto = AudioUploadDto(
191
- filename="test.wav",
192
- content=content,
193
- content_type="audio/wav"
194
- )
195
-
196
- assert dto.size == len(content)
197
-
198
- def test_validation_called_on_init(self):
199
- """Test that validation is called during initialization"""
200
- # This should trigger validation and raise an error
201
- with pytest.raises(ValueError):
202
- AudioUploadDto(
203
- filename="", # Invalid empty filename
204
- content=b"fake_audio_content_" + b"x" * 1000,
205
- content_type="audio/wav"
206
- )
207
-
208
- def test_edge_case_minimum_valid_size(self):
209
- """Test edge case with minimum valid file size"""
210
- min_content = b"x" * 1024 # Exactly 1KB
211
-
212
- dto = AudioUploadDto(
213
- filename="test.wav",
214
- content=min_content,
215
- content_type="audio/wav"
216
- )
217
-
218
- assert dto.size == 1024
219
-
220
- def test_edge_case_maximum_valid_size(self):
221
- """Test edge case with maximum valid file size"""
222
- max_size = 100 * 1024 * 1024 # Exactly 100MB
223
- max_content = b"x" * max_size
224
-
225
- dto = AudioUploadDto(
226
- filename="test.wav",
227
- content=max_content,
228
- content_type="audio/wav"
229
- )
230
-
231
- assert dto.size == max_size
232
-
233
- def test_content_type_mismatch_handling(self):
234
- """Test handling of content type mismatch with filename"""
235
- content = b"fake_audio_content_" + b"x" * 1000
236
-
237
- # This should still work as long as content_type starts with 'audio/'
238
- dto = AudioUploadDto(
239
- filename="test.wav",
240
- content=content,
241
- content_type="audio/mpeg" # Different from .wav extension
242
- )
243
-
244
- assert dto.content_type == "audio/mpeg"
245
- assert dto.file_extension == ".wav"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/application/dtos/test_dto_validation.py DELETED
@@ -1,319 +0,0 @@
1
- """Unit tests for DTO validation utilities"""
2
-
3
- import pytest
4
- from unittest.mock import Mock
5
-
6
- from src.application.dtos.dto_validation import (
7
- ValidationError,
8
- validate_dto,
9
- validation_required,
10
- validate_field,
11
- validate_required,
12
- validate_type,
13
- validate_range,
14
- validate_choices
15
- )
16
-
17
-
18
- class TestValidationError:
19
- """Test cases for ValidationError"""
20
-
21
- def test_validation_error_basic(self):
22
- """Test basic ValidationError creation"""
23
- error = ValidationError("Test error message")
24
-
25
- assert str(error) == "Validation error: Test error message"
26
- assert error.message == "Test error message"
27
- assert error.field is None
28
- assert error.value is None
29
-
30
- def test_validation_error_with_field(self):
31
- """Test ValidationError with field information"""
32
- error = ValidationError("Invalid value", field="test_field", value="invalid_value")
33
-
34
- assert str(error) == "Validation error for field 'test_field': Invalid value"
35
- assert error.message == "Invalid value"
36
- assert error.field == "test_field"
37
- assert error.value == "invalid_value"
38
-
39
- def test_validation_error_without_field_but_with_value(self):
40
- """Test ValidationError without field but with value"""
41
- error = ValidationError("Invalid value", value="test_value")
42
-
43
- assert str(error) == "Validation error: Invalid value"
44
- assert error.message == "Invalid value"
45
- assert error.field is None
46
- assert error.value == "test_value"
47
-
48
-
49
- class TestValidateDto:
50
- """Test cases for validate_dto function"""
51
-
52
- def test_validate_dto_success_with_validate_method(self):
53
- """Test successful DTO validation with _validate method"""
54
- mock_dto = Mock()
55
- mock_dto._validate = Mock()
56
-
57
- result = validate_dto(mock_dto)
58
-
59
- assert result is True
60
- mock_dto._validate.assert_called_once()
61
-
62
- def test_validate_dto_success_without_validate_method(self):
63
- """Test successful DTO validation without _validate method"""
64
- mock_dto = Mock()
65
- del mock_dto._validate # Remove the _validate method
66
-
67
- result = validate_dto(mock_dto)
68
-
69
- assert result is True
70
-
71
- def test_validate_dto_failure_value_error(self):
72
- """Test DTO validation failure with ValueError"""
73
- mock_dto = Mock()
74
- mock_dto._validate.side_effect = ValueError("Validation failed")
75
-
76
- with pytest.raises(ValidationError, match="Validation failed"):
77
- validate_dto(mock_dto)
78
-
79
- def test_validate_dto_failure_unexpected_error(self):
80
- """Test DTO validation failure with unexpected error"""
81
- mock_dto = Mock()
82
- mock_dto._validate.side_effect = RuntimeError("Unexpected error")
83
-
84
- with pytest.raises(ValidationError, match="Validation failed: Unexpected error"):
85
- validate_dto(mock_dto)
86
-
87
-
88
- class TestValidationRequired:
89
- """Test cases for validation_required decorator"""
90
-
91
- def test_validation_required_success(self):
92
- """Test validation_required decorator with successful validation"""
93
- mock_instance = Mock()
94
- mock_instance._validate = Mock()
95
-
96
- @validation_required
97
- def test_method(self):
98
- return "success"
99
-
100
- result = test_method(mock_instance)
101
-
102
- assert result == "success"
103
- mock_instance._validate.assert_called_once()
104
-
105
- def test_validation_required_validation_error(self):
106
- """Test validation_required decorator with validation error"""
107
- mock_instance = Mock()
108
- mock_instance._validate.side_effect = ValueError("Validation failed")
109
-
110
- @validation_required
111
- def test_method(self):
112
- return "success"
113
-
114
- with pytest.raises(ValidationError, match="Validation failed"):
115
- test_method(mock_instance)
116
-
117
- def test_validation_required_method_error(self):
118
- """Test validation_required decorator with method execution error"""
119
- mock_instance = Mock()
120
- mock_instance._validate = Mock()
121
-
122
- @validation_required
123
- def test_method(self):
124
- raise RuntimeError("Method error")
125
-
126
- with pytest.raises(ValidationError, match="Error in test_method: Method error"):
127
- test_method(mock_instance)
128
-
129
- def test_validation_required_preserves_function_metadata(self):
130
- """Test that validation_required preserves function metadata"""
131
- @validation_required
132
- def test_method(self):
133
- """Test method docstring"""
134
- return "success"
135
-
136
- assert test_method.__name__ == "test_method"
137
- assert test_method.__doc__ == "Test method docstring"
138
-
139
-
140
- class TestValidateField:
141
- """Test cases for validate_field function"""
142
-
143
- def test_validate_field_success(self):
144
- """Test successful field validation"""
145
- def is_positive(value):
146
- return value > 0
147
-
148
- result = validate_field(5, "test_field", is_positive)
149
-
150
- assert result == 5
151
-
152
- def test_validate_field_failure_with_custom_message(self):
153
- """Test field validation failure with custom error message"""
154
- def is_positive(value):
155
- return value > 0
156
-
157
- with pytest.raises(ValidationError, match="Custom error message"):
158
- validate_field(-1, "test_field", is_positive, "Custom error message")
159
-
160
- def test_validate_field_failure_with_default_message(self):
161
- """Test field validation failure with default error message"""
162
- def is_positive(value):
163
- return value > 0
164
-
165
- with pytest.raises(ValidationError, match="Invalid value for field 'test_field'"):
166
- validate_field(-1, "test_field", is_positive)
167
-
168
- def test_validate_field_validator_exception(self):
169
- """Test field validation with validator raising exception"""
170
- def failing_validator(value):
171
- raise RuntimeError("Validator error")
172
-
173
- with pytest.raises(ValidationError, match="Validation error for field 'test_field': Validator error"):
174
- validate_field(5, "test_field", failing_validator)
175
-
176
-
177
- class TestValidateRequired:
178
- """Test cases for validate_required function"""
179
-
180
- def test_validate_required_success_with_value(self):
181
- """Test successful required validation with valid value"""
182
- result = validate_required("test_value", "test_field")
183
- assert result == "test_value"
184
-
185
- def test_validate_required_success_with_zero(self):
186
- """Test successful required validation with zero (falsy but valid)"""
187
- result = validate_required(0, "test_field")
188
- assert result == 0
189
-
190
- def test_validate_required_success_with_false(self):
191
- """Test successful required validation with False (falsy but valid)"""
192
- result = validate_required(False, "test_field")
193
- assert result is False
194
-
195
- def test_validate_required_failure_with_none(self):
196
- """Test required validation failure with None"""
197
- with pytest.raises(ValidationError, match="Field 'test_field' is required"):
198
- validate_required(None, "test_field")
199
-
200
- def test_validate_required_failure_with_empty_string(self):
201
- """Test required validation failure with empty string"""
202
- with pytest.raises(ValidationError, match="Field 'test_field' cannot be empty"):
203
- validate_required("", "test_field")
204
-
205
- def test_validate_required_failure_with_empty_list(self):
206
- """Test required validation failure with empty list"""
207
- with pytest.raises(ValidationError, match="Field 'test_field' cannot be empty"):
208
- validate_required([], "test_field")
209
-
210
- def test_validate_required_failure_with_empty_dict(self):
211
- """Test required validation failure with empty dict"""
212
- with pytest.raises(ValidationError, match="Field 'test_field' cannot be empty"):
213
- validate_required({}, "test_field")
214
-
215
-
216
- class TestValidateType:
217
- """Test cases for validate_type function"""
218
-
219
- def test_validate_type_success_single_type(self):
220
- """Test successful type validation with single type"""
221
- result = validate_type("test", "test_field", str)
222
- assert result == "test"
223
-
224
- def test_validate_type_success_multiple_types(self):
225
- """Test successful type validation with multiple types"""
226
- result1 = validate_type("test", "test_field", (str, int))
227
- assert result1 == "test"
228
-
229
- result2 = validate_type(123, "test_field", (str, int))
230
- assert result2 == 123
231
-
232
- def test_validate_type_failure_single_type(self):
233
- """Test type validation failure with single type"""
234
- with pytest.raises(ValidationError, match="Field 'test_field' must be of type str, got int"):
235
- validate_type(123, "test_field", str)
236
-
237
- def test_validate_type_failure_multiple_types(self):
238
- """Test type validation failure with multiple types"""
239
- with pytest.raises(ValidationError, match="Field 'test_field' must be of type str or int, got float"):
240
- validate_type(1.5, "test_field", (str, int))
241
-
242
-
243
- class TestValidateRange:
244
- """Test cases for validate_range function"""
245
-
246
- def test_validate_range_success_within_range(self):
247
- """Test successful range validation within range"""
248
- result = validate_range(5, "test_field", min_value=1, max_value=10)
249
- assert result == 5
250
-
251
- def test_validate_range_success_at_boundaries(self):
252
- """Test successful range validation at boundaries"""
253
- result1 = validate_range(1, "test_field", min_value=1, max_value=10)
254
- assert result1 == 1
255
-
256
- result2 = validate_range(10, "test_field", min_value=1, max_value=10)
257
- assert result2 == 10
258
-
259
- def test_validate_range_success_only_min(self):
260
- """Test successful range validation with only minimum"""
261
- result = validate_range(5, "test_field", min_value=1)
262
- assert result == 5
263
-
264
- def test_validate_range_success_only_max(self):
265
- """Test successful range validation with only maximum"""
266
- result = validate_range(5, "test_field", max_value=10)
267
- assert result == 5
268
-
269
- def test_validate_range_success_no_limits(self):
270
- """Test successful range validation with no limits"""
271
- result = validate_range(5, "test_field")
272
- assert result == 5
273
-
274
- def test_validate_range_failure_below_minimum(self):
275
- """Test range validation failure below minimum"""
276
- with pytest.raises(ValidationError, match="Field 'test_field' must be >= 1, got 0"):
277
- validate_range(0, "test_field", min_value=1, max_value=10)
278
-
279
- def test_validate_range_failure_above_maximum(self):
280
- """Test range validation failure above maximum"""
281
- with pytest.raises(ValidationError, match="Field 'test_field' must be <= 10, got 11"):
282
- validate_range(11, "test_field", min_value=1, max_value=10)
283
-
284
- def test_validate_range_with_float_values(self):
285
- """Test range validation with float values"""
286
- result = validate_range(1.5, "test_field", min_value=1.0, max_value=2.0)
287
- assert result == 1.5
288
-
289
-
290
- class TestValidateChoices:
291
- """Test cases for validate_choices function"""
292
-
293
- def test_validate_choices_success(self):
294
- """Test successful choices validation"""
295
- choices = ["option1", "option2", "option3"]
296
- result = validate_choices("option2", "test_field", choices)
297
- assert result == "option2"
298
-
299
- def test_validate_choices_failure(self):
300
- """Test choices validation failure"""
301
- choices = ["option1", "option2", "option3"]
302
-
303
- with pytest.raises(ValidationError, match="Field 'test_field' must be one of \\['option1', 'option2', 'option3'\\], got 'invalid'"):
304
- validate_choices("invalid", "test_field", choices)
305
-
306
- def test_validate_choices_with_different_types(self):
307
- """Test choices validation with different value types"""
308
- choices = [1, 2, 3, "four"]
309
-
310
- result1 = validate_choices(2, "test_field", choices)
311
- assert result1 == 2
312
-
313
- result2 = validate_choices("four", "test_field", choices)
314
- assert result2 == "four"
315
-
316
- def test_validate_choices_empty_choices(self):
317
- """Test choices validation with empty choices list"""
318
- with pytest.raises(ValidationError, match="Field 'test_field' must be one of \\[\\], got 'value'"):
319
- validate_choices("value", "test_field", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/application/dtos/test_processing_request_dto.py DELETED
@@ -1,383 +0,0 @@
1
- """Unit tests for ProcessingRequestDto"""
2
-
3
- import pytest
4
-
5
- from src.application.dtos.processing_request_dto import ProcessingRequestDto
6
- from src.application.dtos.audio_upload_dto import AudioUploadDto
7
-
8
-
9
- class TestProcessingRequestDto:
10
- """Test cases for ProcessingRequestDto"""
11
-
12
- @pytest.fixture
13
- def sample_audio_upload(self):
14
- """Create sample audio upload DTO"""
15
- return AudioUploadDto(
16
- filename="test_audio.wav",
17
- content=b"fake_audio_content_" + b"x" * 1000,
18
- content_type="audio/wav"
19
- )
20
-
21
- def test_valid_processing_request_dto(self, sample_audio_upload):
22
- """Test creating a valid ProcessingRequestDto"""
23
- dto = ProcessingRequestDto(
24
- audio=sample_audio_upload,
25
- asr_model="whisper-small",
26
- target_language="es",
27
- voice="chatterbox",
28
- speed=1.0,
29
- source_language="en"
30
- )
31
-
32
- assert dto.audio == sample_audio_upload
33
- assert dto.asr_model == "whisper-small"
34
- assert dto.target_language == "es"
35
- assert dto.voice == "kokoro"
36
- assert dto.speed == 1.0
37
- assert dto.source_language == "en"
38
- assert dto.additional_params == {}
39
-
40
- def test_processing_request_dto_with_defaults(self, sample_audio_upload):
41
- """Test creating ProcessingRequestDto with default values"""
42
- dto = ProcessingRequestDto(
43
- audio=sample_audio_upload,
44
- asr_model="whisper-medium",
45
- target_language="fr",
46
- voice="chatterbox"
47
- )
48
-
49
- assert dto.speed == 1.0 # Default speed
50
- assert dto.source_language is None # Default source language
51
- assert dto.additional_params == {} # Default additional params
52
-
53
- def test_processing_request_dto_with_additional_params(self, sample_audio_upload):
54
- """Test creating ProcessingRequestDto with additional parameters"""
55
- additional_params = {
56
- "custom_param": "value",
57
- "another_param": 123
58
- }
59
-
60
- dto = ProcessingRequestDto(
61
- audio=sample_audio_upload,
62
- asr_model="whisper-large",
63
- target_language="de",
64
- voice="chatterbox",
65
- additional_params=additional_params
66
- )
67
-
68
- assert dto.additional_params == additional_params
69
-
70
- def test_invalid_audio_type_validation(self):
71
- """Test validation with invalid audio type"""
72
- with pytest.raises(ValueError, match="Audio must be an AudioUploadDto instance"):
73
- ProcessingRequestDto(
74
- audio="invalid_audio", # Not AudioUploadDto
75
- asr_model="whisper-small",
76
- target_language="es",
77
- voice="chatterbox"
78
- )
79
-
80
- def test_empty_asr_model_validation(self, sample_audio_upload):
81
- """Test validation with empty ASR model"""
82
- with pytest.raises(ValueError, match="ASR model cannot be empty"):
83
- ProcessingRequestDto(
84
- audio=sample_audio_upload,
85
- asr_model="",
86
- target_language="es",
87
- voice="chatterbox"
88
- )
89
-
90
- def test_unsupported_asr_model_validation(self, sample_audio_upload):
91
- """Test validation with unsupported ASR model"""
92
- with pytest.raises(ValueError, match="Unsupported ASR model"):
93
- ProcessingRequestDto(
94
- audio=sample_audio_upload,
95
- asr_model="invalid-model",
96
- target_language="es",
97
- voice="chatterbox"
98
- )
99
-
100
- def test_supported_asr_models(self, sample_audio_upload):
101
- """Test all supported ASR models"""
102
- supported_models = ['whisper-small', 'whisper-medium', 'whisper-large', 'parakeet']
103
-
104
- for model in supported_models:
105
- # Should not raise exception
106
- dto = ProcessingRequestDto(
107
- audio=sample_audio_upload,
108
- asr_model=model,
109
- target_language="es",
110
- voice="chatterbox"
111
- )
112
- assert dto.asr_model == model
113
-
114
- def test_empty_target_language_validation(self, sample_audio_upload):
115
- """Test validation with empty target language"""
116
- with pytest.raises(ValueError, match="Target language cannot be empty"):
117
- ProcessingRequestDto(
118
- audio=sample_audio_upload,
119
- asr_model="whisper-small",
120
- target_language="",
121
- voice="chatterbox"
122
- )
123
-
124
- def test_unsupported_target_language_validation(self, sample_audio_upload):
125
- """Test validation with unsupported target language"""
126
- with pytest.raises(ValueError, match="Unsupported target language"):
127
- ProcessingRequestDto(
128
- audio=sample_audio_upload,
129
- asr_model="whisper-small",
130
- target_language="invalid-lang",
131
- voice="chatterbox"
132
- )
133
-
134
- def test_unsupported_source_language_validation(self, sample_audio_upload):
135
- """Test validation with unsupported source language"""
136
- with pytest.raises(ValueError, match="Unsupported source language"):
137
- ProcessingRequestDto(
138
- audio=sample_audio_upload,
139
- asr_model="whisper-small",
140
- target_language="es",
141
- voice="chatterbox",
142
- source_language="invalid-lang"
143
- )
144
-
145
- def test_supported_languages(self, sample_audio_upload):
146
- """Test all supported languages"""
147
- supported_languages = [
148
- 'en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh',
149
- 'ar', 'hi', 'tr', 'pl', 'nl', 'sv', 'da', 'no', 'fi'
150
- ]
151
-
152
- for lang in supported_languages:
153
- # Should not raise exception
154
- dto = ProcessingRequestDto(
155
- audio=sample_audio_upload,
156
- asr_model="whisper-small",
157
- target_language=lang,
158
- voice="chatterbox",
159
- source_language=lang
160
- )
161
- assert dto.target_language == lang
162
- assert dto.source_language == lang
163
-
164
- def test_empty_voice_validation(self, sample_audio_upload):
165
- """Test validation with empty voice"""
166
- with pytest.raises(ValueError, match="Voice cannot be empty"):
167
- ProcessingRequestDto(
168
- audio=sample_audio_upload,
169
- asr_model="whisper-small",
170
- target_language="es",
171
- voice=""
172
- )
173
-
174
- def test_unsupported_voice_validation(self, sample_audio_upload):
175
- """Test validation with unsupported voice"""
176
- with pytest.raises(ValueError, match="Unsupported voice"):
177
- ProcessingRequestDto(
178
- audio=sample_audio_upload,
179
- asr_model="whisper-small",
180
- target_language="es",
181
- voice="invalid-voice"
182
- )
183
-
184
- def test_supported_voices(self, sample_audio_upload):
185
- """Test all supported voices"""
186
- supported_voices = ['chatterbox']
187
-
188
- for voice in supported_voices:
189
- # Should not raise exception
190
- dto = ProcessingRequestDto(
191
- audio=sample_audio_upload,
192
- asr_model="whisper-small",
193
- target_language="es",
194
- voice=voice
195
- )
196
- assert dto.voice == voice
197
-
198
- def test_speed_range_validation_too_low(self, sample_audio_upload):
199
- """Test validation with speed too low"""
200
- with pytest.raises(ValueError, match="Speed must be between 0.5 and 2.0"):
201
- ProcessingRequestDto(
202
- audio=sample_audio_upload,
203
- asr_model="whisper-small",
204
- target_language="es",
205
- voice="chatterbox",
206
- speed=0.3 # Too low
207
- )
208
-
209
- def test_speed_range_validation_too_high(self, sample_audio_upload):
210
- """Test validation with speed too high"""
211
- with pytest.raises(ValueError, match="Speed must be between 0.5 and 2.0"):
212
- ProcessingRequestDto(
213
- audio=sample_audio_upload,
214
- asr_model="whisper-small",
215
- target_language="es",
216
- voice="chatterbox",
217
- speed=2.5 # Too high
218
- )
219
-
220
- def test_valid_speed_range(self, sample_audio_upload):
221
- """Test valid speed range"""
222
- valid_speeds = [0.5, 1.0, 1.5, 2.0]
223
-
224
- for speed in valid_speeds:
225
- # Should not raise exception
226
- dto = ProcessingRequestDto(
227
- audio=sample_audio_upload,
228
- asr_model="whisper-small",
229
- target_language="es",
230
- voice="chatterbox",
231
- speed=speed
232
- )
233
- assert dto.speed == speed
234
-
235
- def test_invalid_additional_params_type(self, sample_audio_upload):
236
- """Test validation with invalid additional params type"""
237
- with pytest.raises(ValueError, match="Additional params must be a dictionary"):
238
- ProcessingRequestDto(
239
- audio=sample_audio_upload,
240
- asr_model="whisper-small",
241
- target_language="es",
242
- voice="chatterbox",
243
- additional_params="invalid" # Not a dict
244
- )
245
-
246
- def test_requires_translation_property_same_language(self, sample_audio_upload):
247
- """Test requires_translation property when source and target are same"""
248
- dto = ProcessingRequestDto(
249
- audio=sample_audio_upload,
250
- asr_model="whisper-small",
251
- target_language="en",
252
- voice="chatterbox",
253
- source_language="en"
254
- )
255
-
256
- assert dto.requires_translation is False
257
-
258
- def test_requires_translation_property_different_languages(self, sample_audio_upload):
259
- """Test requires_translation property when source and target are different"""
260
- dto = ProcessingRequestDto(
261
- audio=sample_audio_upload,
262
- asr_model="whisper-small",
263
- target_language="es",
264
- voice="chatterbox",
265
- source_language="en"
266
- )
267
-
268
- assert dto.requires_translation is True
269
-
270
- def test_requires_translation_property_no_source(self, sample_audio_upload):
271
- """Test requires_translation property when no source language specified"""
272
- dto = ProcessingRequestDto(
273
- audio=sample_audio_upload,
274
- asr_model="whisper-small",
275
- target_language="es",
276
- voice="chatterbox"
277
- )
278
-
279
- assert dto.requires_translation is True # Assume translation needed
280
-
281
- def test_to_dict_method(self, sample_audio_upload):
282
- """Test to_dict method"""
283
- dto = ProcessingRequestDto(
284
- audio=sample_audio_upload,
285
- asr_model="whisper-small",
286
- target_language="es",
287
- voice="chatterbox",
288
- speed=1.5,
289
- source_language="en",
290
- additional_params={"custom": "value"}
291
- )
292
-
293
- result = dto.to_dict()
294
-
295
- assert result['audio'] == sample_audio_upload.to_dict()
296
- assert result['asr_model'] == "whisper-small"
297
- assert result['target_language'] == "es"
298
- assert result['source_language'] == "en"
299
- assert result['voice'] == "chatterbox"
300
- assert result['speed'] == 1.5
301
- assert result['requires_translation'] is True
302
- assert result['additional_params'] == {"custom": "value"}
303
-
304
- def test_from_dict_method(self, sample_audio_upload):
305
- """Test from_dict class method"""
306
- data = {
307
- 'audio': sample_audio_upload,
308
- 'asr_model': 'whisper-medium',
309
- 'target_language': 'fr',
310
- 'voice': 'dia',
311
- 'speed': 1.2,
312
- 'source_language': 'en',
313
- 'additional_params': {'test': 'value'}
314
- }
315
-
316
- dto = ProcessingRequestDto.from_dict(data)
317
-
318
- assert dto.audio == sample_audio_upload
319
- assert dto.asr_model == 'whisper-medium'
320
- assert dto.target_language == 'fr'
321
- assert dto.voice == 'dia'
322
- assert dto.speed == 1.2
323
- assert dto.source_language == 'en'
324
- assert dto.additional_params == {'test': 'value'}
325
-
326
- def test_from_dict_method_with_audio_dict(self):
327
- """Test from_dict method with audio as dictionary"""
328
- audio_data = {
329
- 'filename': 'test.wav',
330
- 'content': b'fake_content' + b'x' * 1000,
331
- 'content_type': 'audio/wav'
332
- }
333
-
334
- data = {
335
- 'audio': audio_data,
336
- 'asr_model': 'whisper-small',
337
- 'target_language': 'es',
338
- 'voice': 'kokoro'
339
- }
340
-
341
- dto = ProcessingRequestDto.from_dict(data)
342
-
343
- assert isinstance(dto.audio, AudioUploadDto)
344
- assert dto.audio.filename == 'test.wav'
345
- assert dto.audio.content_type == 'audio/wav'
346
-
347
- def test_from_dict_method_with_defaults(self, sample_audio_upload):
348
- """Test from_dict method with default values"""
349
- data = {
350
- 'audio': sample_audio_upload,
351
- 'asr_model': 'whisper-small',
352
- 'target_language': 'es',
353
- 'voice': 'kokoro'
354
- }
355
-
356
- dto = ProcessingRequestDto.from_dict(data)
357
-
358
- assert dto.speed == 1.0 # Default
359
- assert dto.source_language is None # Default
360
- assert dto.additional_params is None # Default
361
-
362
- def test_validation_called_on_init(self, sample_audio_upload):
363
- """Test that validation is called during initialization"""
364
- # This should trigger validation and raise an error
365
- with pytest.raises(ValueError):
366
- ProcessingRequestDto(
367
- audio=sample_audio_upload,
368
- asr_model="", # Invalid empty model
369
- target_language="es",
370
- voice="chatterbox"
371
- )
372
-
373
- def test_additional_params_default_initialization(self, sample_audio_upload):
374
- """Test that additional_params is initialized to empty dict if None"""
375
- dto = ProcessingRequestDto(
376
- audio=sample_audio_upload,
377
- asr_model="whisper-small",
378
- target_language="es",
379
- voice="chatterbox",
380
- additional_params=None
381
- )
382
-
383
- assert dto.additional_params == {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/application/dtos/test_processing_result_dto.py DELETED
@@ -1,436 +0,0 @@
1
- """Unit tests for ProcessingResultDto"""
2
-
3
- import pytest
4
- from datetime import datetime
5
-
6
- from src.application.dtos.processing_result_dto import ProcessingResultDto
7
-
8
-
9
- class TestProcessingResultDto:
10
- """Test cases for ProcessingResultDto"""
11
-
12
- def test_valid_success_processing_result_dto(self):
13
- """Test creating a valid successful ProcessingResultDto"""
14
- dto = ProcessingResultDto(
15
- success=True,
16
- original_text="Hello world",
17
- translated_text="Hola mundo",
18
- audio_path="/tmp/output.wav",
19
- processing_time=5.2,
20
- metadata={"correlation_id": "test-123"}
21
- )
22
-
23
- assert dto.success is True
24
- assert dto.original_text == "Hello world"
25
- assert dto.translated_text == "Hola mundo"
26
- assert dto.audio_path == "/tmp/output.wav"
27
- assert dto.processing_time == 5.2
28
- assert dto.metadata == {"correlation_id": "test-123"}
29
- assert dto.error_message is None
30
- assert dto.error_code is None
31
- assert isinstance(dto.timestamp, datetime)
32
-
33
- def test_valid_error_processing_result_dto(self):
34
- """Test creating a valid error ProcessingResultDto"""
35
- dto = ProcessingResultDto(
36
- success=False,
37
- error_message="Processing failed",
38
- error_code="STT_ERROR",
39
- processing_time=2.1,
40
- metadata={"correlation_id": "test-456"}
41
- )
42
-
43
- assert dto.success is False
44
- assert dto.error_message == "Processing failed"
45
- assert dto.error_code == "STT_ERROR"
46
- assert dto.processing_time == 2.1
47
- assert dto.metadata == {"correlation_id": "test-456"}
48
- assert dto.original_text is None
49
- assert dto.translated_text is None
50
- assert dto.audio_path is None
51
- assert isinstance(dto.timestamp, datetime)
52
-
53
- def test_processing_result_dto_with_defaults(self):
54
- """Test creating ProcessingResultDto with default values"""
55
- dto = ProcessingResultDto(success=True, original_text="Test")
56
-
57
- assert dto.success is True
58
- assert dto.original_text == "Test"
59
- assert dto.translated_text is None
60
- assert dto.audio_path is None
61
- assert dto.processing_time == 0.0
62
- assert dto.error_message is None
63
- assert dto.error_code is None
64
- assert dto.metadata == {}
65
- assert isinstance(dto.timestamp, datetime)
66
-
67
- def test_processing_result_dto_with_explicit_timestamp(self):
68
- """Test creating ProcessingResultDto with explicit timestamp"""
69
- test_timestamp = datetime(2023, 1, 1, 12, 0, 0)
70
-
71
- dto = ProcessingResultDto(
72
- success=True,
73
- original_text="Test",
74
- timestamp=test_timestamp
75
- )
76
-
77
- assert dto.timestamp == test_timestamp
78
-
79
- def test_invalid_success_type_validation(self):
80
- """Test validation with invalid success type"""
81
- with pytest.raises(ValueError, match="Success must be a boolean value"):
82
- ProcessingResultDto(
83
- success="true", # String instead of boolean
84
- original_text="Test"
85
- )
86
-
87
- def test_negative_processing_time_validation(self):
88
- """Test validation with negative processing time"""
89
- with pytest.raises(ValueError, match="Processing time cannot be negative"):
90
- ProcessingResultDto(
91
- success=True,
92
- original_text="Test",
93
- processing_time=-1.0
94
- )
95
-
96
- def test_successful_processing_without_output_validation(self):
97
- """Test validation for successful processing without any output"""
98
- with pytest.raises(ValueError, match="Successful processing must have at least one output"):
99
- ProcessingResultDto(
100
- success=True,
101
- # No original_text, translated_text, or audio_path
102
- )
103
-
104
- def test_failed_processing_without_error_message_validation(self):
105
- """Test validation for failed processing without error message"""
106
- with pytest.raises(ValueError, match="Failed processing must include an error message"):
107
- ProcessingResultDto(
108
- success=False,
109
- # No error_message
110
- )
111
-
112
- def test_invalid_error_code_validation(self):
113
- """Test validation with invalid error code"""
114
- with pytest.raises(ValueError, match="Invalid error code"):
115
- ProcessingResultDto(
116
- success=False,
117
- error_message="Test error",
118
- error_code="INVALID_CODE"
119
- )
120
-
121
- def test_valid_error_codes(self):
122
- """Test all valid error codes"""
123
- valid_error_codes = [
124
- 'STT_ERROR', 'TRANSLATION_ERROR', 'TTS_ERROR',
125
- 'AUDIO_FORMAT_ERROR', 'VALIDATION_ERROR', 'SYSTEM_ERROR'
126
- ]
127
-
128
- for error_code in valid_error_codes:
129
- # Should not raise exception
130
- dto = ProcessingResultDto(
131
- success=False,
132
- error_message="Test error",
133
- error_code=error_code
134
- )
135
- assert dto.error_code == error_code
136
-
137
- def test_invalid_metadata_type_validation(self):
138
- """Test validation with invalid metadata type"""
139
- with pytest.raises(ValueError, match="Metadata must be a dictionary"):
140
- ProcessingResultDto(
141
- success=True,
142
- original_text="Test",
143
- metadata="invalid" # String instead of dict
144
- )
145
-
146
- def test_has_text_output_property(self):
147
- """Test has_text_output property"""
148
- # With original text only
149
- dto1 = ProcessingResultDto(success=True, original_text="Test")
150
- assert dto1.has_text_output is True
151
-
152
- # With translated text only
153
- dto2 = ProcessingResultDto(success=True, translated_text="Prueba")
154
- assert dto2.has_text_output is True
155
-
156
- # With both texts
157
- dto3 = ProcessingResultDto(success=True, original_text="Test", translated_text="Prueba")
158
- assert dto3.has_text_output is True
159
-
160
- # With no text
161
- dto4 = ProcessingResultDto(success=True, audio_path="/tmp/test.wav")
162
- assert dto4.has_text_output is False
163
-
164
- def test_has_audio_output_property(self):
165
- """Test has_audio_output property"""
166
- # With audio path
167
- dto1 = ProcessingResultDto(success=True, audio_path="/tmp/test.wav")
168
- assert dto1.has_audio_output is True
169
-
170
- # Without audio path
171
- dto2 = ProcessingResultDto(success=True, original_text="Test")
172
- assert dto2.has_audio_output is False
173
-
174
- def test_is_complete_property(self):
175
- """Test is_complete property"""
176
- # Successful processing
177
- dto1 = ProcessingResultDto(success=True, original_text="Test")
178
- assert dto1.is_complete is True
179
-
180
- # Failed processing with error message
181
- dto2 = ProcessingResultDto(success=False, error_message="Error")
182
- assert dto2.is_complete is True
183
-
184
- # This shouldn't happen due to validation, but test the logic
185
- dto3 = ProcessingResultDto.__new__(ProcessingResultDto)
186
- dto3.success = False
187
- dto3.error_message = None
188
- assert dto3.is_complete is False
189
-
190
- def test_add_metadata_method(self):
191
- """Test add_metadata method"""
192
- dto = ProcessingResultDto(success=True, original_text="Test")
193
-
194
- dto.add_metadata("key1", "value1")
195
- dto.add_metadata("key2", 123)
196
-
197
- assert dto.metadata["key1"] == "value1"
198
- assert dto.metadata["key2"] == 123
199
-
200
- def test_add_metadata_method_with_none_metadata(self):
201
- """Test add_metadata method when metadata is None"""
202
- dto = ProcessingResultDto.__new__(ProcessingResultDto)
203
- dto.success = True
204
- dto.original_text = "Test"
205
- dto.metadata = None
206
-
207
- dto.add_metadata("key", "value")
208
-
209
- assert dto.metadata == {"key": "value"}
210
-
211
- def test_get_metadata_method(self):
212
- """Test get_metadata method"""
213
- dto = ProcessingResultDto(
214
- success=True,
215
- original_text="Test",
216
- metadata={"existing_key": "existing_value"}
217
- )
218
-
219
- # Get existing key
220
- assert dto.get_metadata("existing_key") == "existing_value"
221
-
222
- # Get non-existing key with default
223
- assert dto.get_metadata("non_existing", "default") == "default"
224
-
225
- # Get non-existing key without default
226
- assert dto.get_metadata("non_existing") is None
227
-
228
- def test_get_metadata_method_with_none_metadata(self):
229
- """Test get_metadata method when metadata is None"""
230
- dto = ProcessingResultDto.__new__(ProcessingResultDto)
231
- dto.success = True
232
- dto.original_text = "Test"
233
- dto.metadata = None
234
-
235
- assert dto.get_metadata("key", "default") == "default"
236
- assert dto.get_metadata("key") is None
237
-
238
- def test_to_dict_method(self):
239
- """Test to_dict method"""
240
- test_timestamp = datetime(2023, 1, 1, 12, 0, 0)
241
-
242
- dto = ProcessingResultDto(
243
- success=True,
244
- original_text="Hello world",
245
- translated_text="Hola mundo",
246
- audio_path="/tmp/output.wav",
247
- processing_time=5.2,
248
- error_message=None,
249
- error_code=None,
250
- metadata={"correlation_id": "test-123"},
251
- timestamp=test_timestamp
252
- )
253
-
254
- result = dto.to_dict()
255
-
256
- assert result['success'] is True
257
- assert result['original_text'] == "Hello world"
258
- assert result['translated_text'] == "Hola mundo"
259
- assert result['audio_path'] == "/tmp/output.wav"
260
- assert result['processing_time'] == 5.2
261
- assert result['error_message'] is None
262
- assert result['error_code'] is None
263
- assert result['metadata'] == {"correlation_id": "test-123"}
264
- assert result['timestamp'] == test_timestamp.isoformat()
265
- assert result['has_text_output'] is True
266
- assert result['has_audio_output'] is True
267
- assert result['is_complete'] is True
268
-
269
- def test_to_dict_method_with_none_timestamp(self):
270
- """Test to_dict method with None timestamp"""
271
- dto = ProcessingResultDto.__new__(ProcessingResultDto)
272
- dto.success = True
273
- dto.original_text = "Test"
274
- dto.translated_text = None
275
- dto.audio_path = None
276
- dto.processing_time = 0.0
277
- dto.error_message = None
278
- dto.error_code = None
279
- dto.metadata = {}
280
- dto.timestamp = None
281
-
282
- result = dto.to_dict()
283
-
284
- assert result['timestamp'] is None
285
-
286
- def test_success_result_class_method(self):
287
- """Test success_result class method"""
288
- result = ProcessingResultDto.success_result(
289
- original_text="Hello world",
290
- translated_text="Hola mundo",
291
- audio_path="/tmp/output.wav",
292
- processing_time=3.5,
293
- metadata={"test": "value"}
294
- )
295
-
296
- assert isinstance(result, ProcessingResultDto)
297
- assert result.success is True
298
- assert result.original_text == "Hello world"
299
- assert result.translated_text == "Hola mundo"
300
- assert result.audio_path == "/tmp/output.wav"
301
- assert result.processing_time == 3.5
302
- assert result.metadata == {"test": "value"}
303
- assert result.error_message is None
304
- assert result.error_code is None
305
-
306
- def test_success_result_class_method_with_defaults(self):
307
- """Test success_result class method with default values"""
308
- result = ProcessingResultDto.success_result(original_text="Test")
309
-
310
- assert result.success is True
311
- assert result.original_text == "Test"
312
- assert result.translated_text is None
313
- assert result.audio_path is None
314
- assert result.processing_time == 0.0
315
- assert result.metadata is None
316
-
317
- def test_error_result_class_method(self):
318
- """Test error_result class method"""
319
- result = ProcessingResultDto.error_result(
320
- error_message="Processing failed",
321
- error_code="STT_ERROR",
322
- processing_time=2.1,
323
- metadata={"correlation_id": "test-456"}
324
- )
325
-
326
- assert isinstance(result, ProcessingResultDto)
327
- assert result.success is False
328
- assert result.error_message == "Processing failed"
329
- assert result.error_code == "STT_ERROR"
330
- assert result.processing_time == 2.1
331
- assert result.metadata == {"correlation_id": "test-456"}
332
- assert result.original_text is None
333
- assert result.translated_text is None
334
- assert result.audio_path is None
335
-
336
- def test_error_result_class_method_with_defaults(self):
337
- """Test error_result class method with default values"""
338
- result = ProcessingResultDto.error_result("Test error")
339
-
340
- assert result.success is False
341
- assert result.error_message == "Test error"
342
- assert result.error_code is None
343
- assert result.processing_time == 0.0
344
- assert result.metadata is None
345
-
346
- def test_from_dict_class_method(self):
347
- """Test from_dict class method"""
348
- test_timestamp = datetime(2023, 1, 1, 12, 0, 0)
349
-
350
- data = {
351
- 'success': True,
352
- 'original_text': 'Hello world',
353
- 'translated_text': 'Hola mundo',
354
- 'audio_path': '/tmp/output.wav',
355
- 'processing_time': 5.2,
356
- 'error_message': None,
357
- 'error_code': None,
358
- 'metadata': {'correlation_id': 'test-123'},
359
- 'timestamp': test_timestamp.isoformat()
360
- }
361
-
362
- dto = ProcessingResultDto.from_dict(data)
363
-
364
- assert dto.success is True
365
- assert dto.original_text == 'Hello world'
366
- assert dto.translated_text == 'Hola mundo'
367
- assert dto.audio_path == '/tmp/output.wav'
368
- assert dto.processing_time == 5.2
369
- assert dto.error_message is None
370
- assert dto.error_code is None
371
- assert dto.metadata == {'correlation_id': 'test-123'}
372
- assert dto.timestamp == test_timestamp
373
-
374
- def test_from_dict_class_method_with_z_timestamp(self):
375
- """Test from_dict class method with Z-suffixed timestamp"""
376
- data = {
377
- 'success': True,
378
- 'original_text': 'Test',
379
- 'timestamp': '2023-01-01T12:00:00Z'
380
- }
381
-
382
- dto = ProcessingResultDto.from_dict(data)
383
-
384
- assert dto.timestamp == datetime(2023, 1, 1, 12, 0, 0)
385
-
386
- def test_from_dict_class_method_with_defaults(self):
387
- """Test from_dict class method with default values"""
388
- data = {
389
- 'success': True,
390
- 'original_text': 'Test'
391
- }
392
-
393
- dto = ProcessingResultDto.from_dict(data)
394
-
395
- assert dto.success is True
396
- assert dto.original_text == 'Test'
397
- assert dto.translated_text is None
398
- assert dto.audio_path is None
399
- assert dto.processing_time == 0.0
400
- assert dto.error_message is None
401
- assert dto.error_code is None
402
- assert dto.metadata is None
403
- assert dto.timestamp is None
404
-
405
- def test_validation_called_on_init(self):
406
- """Test that validation is called during initialization"""
407
- # This should trigger validation and raise an error
408
- with pytest.raises(ValueError):
409
- ProcessingResultDto(
410
- success="invalid", # Invalid type
411
- original_text="Test"
412
- )
413
-
414
- def test_metadata_default_initialization(self):
415
- """Test that metadata is initialized to empty dict if None"""
416
- dto = ProcessingResultDto(
417
- success=True,
418
- original_text="Test",
419
- metadata=None
420
- )
421
-
422
- assert dto.metadata == {}
423
-
424
- def test_timestamp_default_initialization(self):
425
- """Test that timestamp is initialized to current time if None"""
426
- before = datetime.utcnow()
427
-
428
- dto = ProcessingResultDto(
429
- success=True,
430
- original_text="Test",
431
- timestamp=None
432
- )
433
-
434
- after = datetime.utcnow()
435
-
436
- assert before <= dto.timestamp <= after
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/application/error_handling/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Tests for application error handling."""
 
 
tests/unit/application/error_handling/test_error_mapper.py DELETED
@@ -1,155 +0,0 @@
1
- """Tests for error mapper functionality."""
2
-
3
- import pytest
4
- from unittest.mock import Mock
5
-
6
- from src.application.error_handling.error_mapper import (
7
- ErrorMapper, ErrorMapping, ErrorSeverity, ErrorCategory
8
- )
9
- from src.domain.exceptions import (
10
- InvalidAudioFormatException,
11
- TranslationFailedException,
12
- SpeechRecognitionException,
13
- SpeechSynthesisException
14
- )
15
-
16
-
17
- class TestErrorMapper:
18
- """Test cases for ErrorMapper."""
19
-
20
- def setup_method(self):
21
- """Set up test fixtures."""
22
- self.error_mapper = ErrorMapper()
23
-
24
- def test_map_domain_exception(self):
25
- """Test mapping of domain exceptions."""
26
- exception = InvalidAudioFormatException("Unsupported format: xyz")
27
- context = {
28
- 'file_name': 'test.xyz',
29
- 'correlation_id': 'test-123'
30
- }
31
-
32
- mapping = self.error_mapper.map_exception(exception, context)
33
-
34
- assert mapping.error_code == "INVALID_AUDIO_FORMAT"
35
- assert mapping.severity == ErrorSeverity.MEDIUM
36
- assert mapping.category == ErrorCategory.VALIDATION
37
- assert "supported format" in mapping.user_message.lower()
38
- assert len(mapping.recovery_suggestions) > 0
39
-
40
- def test_map_translation_exception(self):
41
- """Test mapping of translation exceptions."""
42
- exception = TranslationFailedException("Translation service unavailable")
43
-
44
- mapping = self.error_mapper.map_exception(exception)
45
-
46
- assert mapping.error_code == "TRANSLATION_FAILED"
47
- assert mapping.severity == ErrorSeverity.HIGH
48
- assert mapping.category == ErrorCategory.PROCESSING
49
- assert "translation failed" in mapping.user_message.lower()
50
-
51
- def test_map_speech_recognition_exception(self):
52
- """Test mapping of speech recognition exceptions."""
53
- exception = SpeechRecognitionException("Audio quality too poor")
54
- context = {'provider': 'whisper'}
55
-
56
- mapping = self.error_mapper.map_exception(exception, context)
57
-
58
- assert mapping.error_code == "SPEECH_RECOGNITION_FAILED"
59
- assert mapping.severity == ErrorSeverity.HIGH
60
- assert mapping.category == ErrorCategory.PROCESSING
61
- assert any("whisper" in suggestion for suggestion in mapping.recovery_suggestions)
62
-
63
- def test_map_speech_synthesis_exception(self):
64
- """Test mapping of speech synthesis exceptions."""
65
- exception = SpeechSynthesisException("Voice not available")
66
-
67
- mapping = self.error_mapper.map_exception(exception)
68
-
69
- assert mapping.error_code == "SPEECH_SYNTHESIS_FAILED"
70
- assert mapping.severity == ErrorSeverity.HIGH
71
- assert mapping.category == ErrorCategory.PROCESSING
72
-
73
- def test_map_unknown_exception(self):
74
- """Test mapping of unknown exceptions."""
75
- exception = RuntimeError("Unknown error")
76
-
77
- mapping = self.error_mapper.map_exception(exception)
78
-
79
- assert mapping.error_code == "UNKNOWN_ERROR"
80
- assert mapping.severity == ErrorSeverity.CRITICAL
81
- assert mapping.category == ErrorCategory.SYSTEM
82
-
83
- def test_context_enhancement(self):
84
- """Test context enhancement of error mappings."""
85
- exception = ValueError("Invalid parameter")
86
- context = {
87
- 'file_name': 'large_file.wav',
88
- 'file_size': 100 * 1024 * 1024, # 100MB
89
- 'correlation_id': 'test-456',
90
- 'operation': 'audio_processing'
91
- }
92
-
93
- mapping = self.error_mapper.map_exception(exception, context)
94
-
95
- assert 'large_file.wav' in mapping.user_message
96
- assert 'test-456' in mapping.technical_details
97
- assert any("smaller file" in suggestion for suggestion in mapping.recovery_suggestions)
98
-
99
- def test_get_error_code_from_exception(self):
100
- """Test getting error code from exception."""
101
- exception = TranslationFailedException("Test error")
102
-
103
- error_code = self.error_mapper.get_error_code_from_exception(exception)
104
-
105
- assert error_code == "TRANSLATION_FAILED"
106
-
107
- def test_get_user_message_from_exception(self):
108
- """Test getting user message from exception."""
109
- exception = InvalidAudioFormatException("Test error")
110
-
111
- message = self.error_mapper.get_user_message_from_exception(exception)
112
-
113
- assert "supported" in message.lower()
114
- assert "format" in message.lower()
115
-
116
- def test_get_recovery_suggestions(self):
117
- """Test getting recovery suggestions."""
118
- exception = SpeechRecognitionException("Test error")
119
-
120
- suggestions = self.error_mapper.get_recovery_suggestions(exception)
121
-
122
- assert len(suggestions) > 0
123
- assert any("audio quality" in suggestion.lower() for suggestion in suggestions)
124
-
125
- def test_add_custom_mapping(self):
126
- """Test adding custom error mapping."""
127
- custom_mapping = ErrorMapping(
128
- user_message="Custom error message",
129
- error_code="CUSTOM_ERROR",
130
- severity=ErrorSeverity.LOW,
131
- category=ErrorCategory.VALIDATION
132
- )
133
-
134
- self.error_mapper.add_custom_mapping(CustomException, custom_mapping)
135
-
136
- exception = CustomException("Test")
137
- mapping = self.error_mapper.map_exception(exception)
138
-
139
- assert mapping.error_code == "CUSTOM_ERROR"
140
- assert mapping.user_message == "Custom error message"
141
-
142
- def test_get_all_error_codes(self):
143
- """Test getting all error codes."""
144
- error_codes = self.error_mapper.get_all_error_codes()
145
-
146
- assert "INVALID_AUDIO_FORMAT" in error_codes
147
- assert "TRANSLATION_FAILED" in error_codes
148
- assert "SPEECH_RECOGNITION_FAILED" in error_codes
149
- assert "SPEECH_SYNTHESIS_FAILED" in error_codes
150
- assert "UNKNOWN_ERROR" in error_codes
151
-
152
-
153
- class CustomException(Exception):
154
- """Custom exception for testing."""
155
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/application/error_handling/test_structured_logger.py DELETED
@@ -1,298 +0,0 @@
1
- """Tests for structured logger functionality."""
2
-
3
- import pytest
4
- import json
5
- import logging
6
- from unittest.mock import Mock, patch
7
-
8
- from src.application.error_handling.structured_logger import (
9
- StructuredLogger, LogContext, JsonFormatter, ContextFormatter,
10
- get_structured_logger, set_correlation_id, get_correlation_id,
11
- generate_correlation_id
12
- )
13
-
14
-
15
- class TestLogContext:
16
- """Test cases for LogContext."""
17
-
18
- def test_log_context_creation(self):
19
- """Test creating log context."""
20
- context = LogContext(
21
- correlation_id="test-123",
22
- operation="test_operation",
23
- component="test_component"
24
- )
25
-
26
- assert context.correlation_id == "test-123"
27
- assert context.operation == "test_operation"
28
- assert context.component == "test_component"
29
-
30
- def test_log_context_to_dict(self):
31
- """Test converting log context to dictionary."""
32
- context = LogContext(
33
- correlation_id="test-123",
34
- operation="test_operation",
35
- metadata={"key": "value"}
36
- )
37
-
38
- context_dict = context.to_dict()
39
-
40
- assert context_dict["correlation_id"] == "test-123"
41
- assert context_dict["operation"] == "test_operation"
42
- assert context_dict["metadata"] == {"key": "value"}
43
- assert "user_id" not in context_dict # None values should be excluded
44
-
45
-
46
- class TestStructuredLogger:
47
- """Test cases for StructuredLogger."""
48
-
49
- def setup_method(self):
50
- """Set up test fixtures."""
51
- self.logger = StructuredLogger("test_logger", enable_json_logging=False)
52
-
53
- def test_logger_creation(self):
54
- """Test creating structured logger."""
55
- assert self.logger.logger.name == "test_logger"
56
- assert not self.logger.enable_json_logging
57
-
58
- def test_debug_logging(self):
59
- """Test debug logging."""
60
- context = LogContext(correlation_id="test-123", operation="test_op")
61
-
62
- with patch.object(self.logger.logger, 'debug') as mock_debug:
63
- self.logger.info("Test debug message", context=context)
64
-
65
- mock_debug.assert_called_once()
66
- args, kwargs = mock_debug.call_args
67
- assert "Test debug message" in args[0]
68
- assert "extra" in kwargs
69
-
70
- def test_info_logging(self):
71
- """Test info logging."""
72
- context = LogContext(correlation_id="test-123")
73
- extra = {"key": "value"}
74
-
75
- with patch.object(self.logger.logger, 'info') as mock_info:
76
- self.logger.info("Test info message", context=context, extra=extra)
77
-
78
- mock_info.assert_called_once()
79
- args, kwargs = mock_info.call_args
80
- assert "Test info message" in args[0]
81
- assert kwargs["extra"]["extra"] == extra
82
-
83
- def test_error_logging_with_exception(self):
84
- """Test error logging with exception."""
85
- context = LogContext(correlation_id="test-123")
86
- exception = ValueError("Test error")
87
-
88
- with patch.object(self.logger.logger, 'error') as mock_error:
89
- self.logger.error("Test error message", context=context, exception=exception)
90
-
91
- mock_error.assert_called_once()
92
- args, kwargs = mock_error.call_args
93
- assert "Test error message" in args[0]
94
- assert kwargs["extra"]["exception"]["type"] == "ValueError"
95
- assert kwargs["extra"]["exception"]["message"] == "Test error"
96
-
97
- def test_log_operation_start(self):
98
- """Test logging operation start."""
99
- extra = {"param": "value"}
100
-
101
- with patch.object(self.logger.logger, 'info') as mock_info:
102
- correlation_id = self.logger.log_operation_start("test_operation", extra=extra)
103
-
104
- assert correlation_id is not None
105
- mock_info.assert_called_once()
106
- args, kwargs = mock_info.call_args
107
- assert "Operation started: test_operation" in args[0]
108
-
109
- def test_log_operation_end_success(self):
110
- """Test logging successful operation end."""
111
- correlation_id = "test-123"
112
-
113
- with patch.object(self.logger.logger, 'info') as mock_info:
114
- self.logger.log_operation_end(
115
- "test_operation",
116
- correlation_id,
117
- success=True,
118
- duration=1.5
119
- )
120
-
121
- mock_info.assert_called_once()
122
- args, kwargs = mock_info.call_args
123
- assert "completed successfully" in args[0]
124
- assert kwargs["extra"]["extra"]["success"] is True
125
- assert kwargs["extra"]["extra"]["duration_seconds"] == 1.5
126
-
127
- def test_log_operation_end_failure(self):
128
- """Test logging failed operation end."""
129
- correlation_id = "test-123"
130
-
131
- with patch.object(self.logger.logger, 'error') as mock_error:
132
- self.logger.log_operation_end(
133
- "test_operation",
134
- correlation_id,
135
- success=False
136
- )
137
-
138
- mock_error.assert_called_once()
139
- args, kwargs = mock_error.call_args
140
- assert "failed" in args[0]
141
- assert kwargs["extra"]["extra"]["success"] is False
142
-
143
- def test_log_performance_metric(self):
144
- """Test logging performance metric."""
145
- context = LogContext(correlation_id="test-123")
146
-
147
- with patch.object(self.logger.logger, 'info') as mock_info:
148
- self.logger.log_performance_metric(
149
- "response_time",
150
- 150.5,
151
- "ms",
152
- context=context
153
- )
154
-
155
- mock_info.assert_called_once()
156
- args, kwargs = mock_info.call_args
157
- assert "Performance metric: response_time=150.5 ms" in args[0]
158
- assert kwargs["extra"]["extra"]["metric"]["name"] == "response_time"
159
- assert kwargs["extra"]["extra"]["metric"]["value"] == 150.5
160
- assert kwargs["extra"]["extra"]["metric"]["unit"] == "ms"
161
-
162
-
163
- class TestJsonFormatter:
164
- """Test cases for JsonFormatter."""
165
-
166
- def setup_method(self):
167
- """Set up test fixtures."""
168
- self.formatter = JsonFormatter()
169
-
170
- def test_format_log_record(self):
171
- """Test formatting log record as JSON."""
172
- record = logging.LogRecord(
173
- name="test_logger",
174
- level=logging.INFO,
175
- pathname="test.py",
176
- lineno=10,
177
- msg="Test message",
178
- args=(),
179
- exc_info=None
180
- )
181
-
182
- # Add extra data
183
- record.extra = {
184
- "correlation_id": "test-123",
185
- "operation": "test_op"
186
- }
187
-
188
- formatted = self.formatter.format(record)
189
-
190
- # Should be valid JSON
191
- log_data = json.loads(formatted)
192
- assert log_data["message"] == "Test message"
193
- assert log_data["level"] == "INFO"
194
- assert log_data["correlation_id"] == "test-123"
195
- assert log_data["operation"] == "test_op"
196
-
197
- def test_format_error_handling(self):
198
- """Test formatter error handling."""
199
- record = logging.LogRecord(
200
- name="test_logger",
201
- level=logging.INFO,
202
- pathname="test.py",
203
- lineno=10,
204
- msg="Test message",
205
- args=(),
206
- exc_info=None
207
- )
208
-
209
- # Add problematic extra data that can't be JSON serialized
210
- record.extra = {
211
- "correlation_id": "test-123",
212
- "problematic_data": object() # Can't be JSON serialized
213
- }
214
-
215
- formatted = self.formatter.format(record)
216
-
217
- # Should still work and include error message
218
- assert "Test message" in formatted
219
-
220
-
221
- class TestContextFormatter:
222
- """Test cases for ContextFormatter."""
223
-
224
- def setup_method(self):
225
- """Set up test fixtures."""
226
- self.formatter = ContextFormatter()
227
-
228
- def test_format_with_correlation_id(self):
229
- """Test formatting with correlation ID."""
230
- record = logging.LogRecord(
231
- name="test_logger",
232
- level=logging.INFO,
233
- pathname="test.py",
234
- lineno=10,
235
- msg="Test message",
236
- args=(),
237
- exc_info=None
238
- )
239
-
240
- record.extra = {"correlation_id": "test-123"}
241
-
242
- formatted = self.formatter.format(record)
243
-
244
- assert "[test-123]" in formatted
245
- assert "Test message" in formatted
246
-
247
- def test_format_with_operation(self):
248
- """Test formatting with operation context."""
249
- record = logging.LogRecord(
250
- name="test_logger",
251
- level=logging.INFO,
252
- pathname="test.py",
253
- lineno=10,
254
- msg="Test message",
255
- args=(),
256
- exc_info=None
257
- )
258
-
259
- record.extra = {
260
- "correlation_id": "test-123",
261
- "operation": "test_operation"
262
- }
263
-
264
- formatted = self.formatter.format(record)
265
-
266
- assert "[test_operation]" in formatted
267
- assert "Test message" in formatted
268
-
269
-
270
- class TestUtilityFunctions:
271
- """Test cases for utility functions."""
272
-
273
- def test_get_structured_logger(self):
274
- """Test getting structured logger."""
275
- logger = get_structured_logger("test_logger")
276
-
277
- assert isinstance(logger, StructuredLogger)
278
- assert logger.logger.name == "test_logger"
279
-
280
- def test_correlation_id_context(self):
281
- """Test correlation ID context management."""
282
- # Initially should be None
283
- assert get_correlation_id() is None
284
-
285
- # Set correlation ID
286
- set_correlation_id("test-123")
287
- assert get_correlation_id() == "test-123"
288
-
289
- def test_generate_correlation_id(self):
290
- """Test generating correlation ID."""
291
- correlation_id = generate_correlation_id()
292
-
293
- assert correlation_id is not None
294
- assert len(correlation_id) > 0
295
-
296
- # Should generate different IDs
297
- another_id = generate_correlation_id()
298
- assert correlation_id != another_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/application/services/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Unit tests for application services"""
 
 
tests/unit/application/services/test_audio_processing_service.py DELETED
@@ -1,475 +0,0 @@
1
- """Unit tests for AudioProcessingApplicationService"""
2
-
3
- import pytest
4
- import tempfile
5
- import os
6
- import time
7
- from unittest.mock import Mock, MagicMock, patch, call
8
- from contextlib import contextmanager
9
-
10
- from src.application.services.audio_processing_service import AudioProcessingApplicationService
11
- from src.application.dtos.audio_upload_dto import AudioUploadDto
12
- from src.application.dtos.processing_request_dto import ProcessingRequestDto
13
- from src.application.dtos.processing_result_dto import ProcessingResultDto
14
- from src.domain.models.audio_content import AudioContent
15
- from src.domain.models.text_content import TextContent
16
- from src.domain.models.translation_request import TranslationRequest
17
- from src.domain.models.speech_synthesis_request import SpeechSynthesisRequest
18
- from src.domain.models.voice_settings import VoiceSettings
19
- from src.domain.exceptions import (
20
- AudioProcessingException,
21
- SpeechRecognitionException,
22
- TranslationFailedException,
23
- SpeechSynthesisException
24
- )
25
- from src.infrastructure.config.app_config import AppConfig
26
- from src.infrastructure.config.dependency_container import DependencyContainer
27
-
28
-
29
- class TestAudioProcessingApplicationService:
30
- """Test cases for AudioProcessingApplicationService"""
31
-
32
- @pytest.fixture
33
- def mock_container(self):
34
- """Create mock dependency container"""
35
- container = Mock(spec=DependencyContainer)
36
-
37
- # Mock providers
38
- mock_stt_provider = Mock()
39
- mock_translation_provider = Mock()
40
- mock_tts_provider = Mock()
41
-
42
- container.get_stt_provider.return_value = mock_stt_provider
43
- container.get_translation_provider.return_value = mock_translation_provider
44
- container.get_tts_provider.return_value = mock_tts_provider
45
-
46
- return container
47
-
48
- @pytest.fixture
49
- def mock_config(self):
50
- """Create mock application config"""
51
- config = Mock(spec=AppConfig)
52
-
53
- # Mock configuration methods
54
- config.get_logging_config.return_value = {
55
- 'level': 'INFO',
56
- 'enable_file_logging': False,
57
- 'log_file_path': '/tmp/test.log',
58
- 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
59
- }
60
-
61
- config.get_processing_config.return_value = {
62
- 'max_file_size_mb': 100,
63
- 'supported_audio_formats': ['wav', 'mp3', 'flac', 'ogg', 'm4a'],
64
- 'temp_dir': '/tmp',
65
- 'cleanup_temp_files': True,
66
- 'processing_timeout_seconds': 300
67
- }
68
-
69
- config.get_stt_config.return_value = {
70
- 'preferred_providers': ['whisper-small', 'whisper-medium']
71
- }
72
-
73
- config.get_tts_config.return_value = {
74
- 'preferred_providers': ['chatterbox']
75
- }
76
-
77
- return config
78
-
79
- @pytest.fixture
80
- def sample_audio_upload(self):
81
- """Create sample audio upload DTO"""
82
- return AudioUploadDto(
83
- filename="test_audio.wav",
84
- content=b"fake_audio_content_" + b"x" * 1000, # 1KB+ of fake audio
85
- content_type="audio/wav"
86
- )
87
-
88
- @pytest.fixture
89
- def sample_processing_request(self, sample_audio_upload):
90
- """Create sample processing request DTO"""
91
- return ProcessingRequestDto(
92
- audio=sample_audio_upload,
93
- asr_model="whisper-small",
94
- target_language="es",
95
- voice="chatterbox",
96
- speed=1.0,
97
- source_language="en"
98
- )
99
-
100
- @pytest.fixture
101
- def service(self, mock_container, mock_config):
102
- """Create AudioProcessingApplicationService instance"""
103
- mock_container.resolve.return_value = mock_config
104
- return AudioProcessingApplicationService(mock_container, mock_config)
105
-
106
- def test_initialization(self, mock_container, mock_config):
107
- """Test service initialization"""
108
- service = AudioProcessingApplicationService(mock_container, mock_config)
109
-
110
- assert service._container == mock_container
111
- assert service._config == mock_config
112
- assert service._temp_files == {}
113
- assert service._error_mapper is not None
114
- assert service._recovery_manager is not None
115
-
116
- def test_initialization_without_config(self, mock_container, mock_config):
117
- """Test service initialization without explicit config"""
118
- mock_container.resolve.return_value = mock_config
119
-
120
- service = AudioProcessingApplicationService(mock_container)
121
-
122
- assert service._container == mock_container
123
- assert service._config == mock_config
124
- mock_container.resolve.assert_called_once_with(AppConfig)
125
-
126
- @patch('src.application.services.audio_processing_service.get_structured_logger')
127
- def test_setup_logging_success(self, mock_logger, service, mock_config):
128
- """Test successful logging setup"""
129
- mock_config.get_logging_config.return_value = {
130
- 'level': 'DEBUG',
131
- 'enable_file_logging': True,
132
- 'log_file_path': '/tmp/test.log',
133
- 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
134
- }
135
-
136
- service._setup_logging()
137
-
138
- # Verify logging configuration was retrieved
139
- mock_config.get_logging_config.assert_called_once()
140
-
141
- @patch('src.application.services.audio_processing_service.get_structured_logger')
142
- def test_setup_logging_failure(self, mock_logger, service, mock_config):
143
- """Test logging setup failure handling"""
144
- mock_config.get_logging_config.side_effect = Exception("Config error")
145
-
146
- # Should not raise exception
147
- service._setup_logging()
148
-
149
- # Warning should be logged
150
- mock_logger.return_value.warning.assert_called_once()
151
-
152
- def test_validate_request_success(self, service, sample_processing_request):
153
- """Test successful request validation"""
154
- # Should not raise exception
155
- service._validate_request(sample_processing_request)
156
-
157
- def test_validate_request_invalid_type(self, service):
158
- """Test request validation with invalid type"""
159
- with pytest.raises(ValueError, match="Request must be a ProcessingRequestDto instance"):
160
- service._validate_request("invalid_request")
161
-
162
- def test_validate_request_file_too_large(self, service, sample_processing_request, mock_config):
163
- """Test request validation with file too large"""
164
- mock_config.get_processing_config.return_value['max_file_size_mb'] = 0.001 # Very small limit
165
-
166
- with pytest.raises(ValueError, match="Audio file too large"):
167
- service._validate_request(sample_processing_request)
168
-
169
- def test_validate_request_unsupported_format(self, service, sample_processing_request, mock_config):
170
- """Test request validation with unsupported format"""
171
- sample_processing_request.audio.filename = "test.xyz"
172
- mock_config.get_processing_config.return_value['supported_audio_formats'] = ['wav', 'mp3']
173
-
174
- with pytest.raises(ValueError, match="Unsupported audio format"):
175
- service._validate_request(sample_processing_request)
176
-
177
- @patch('os.makedirs')
178
- @patch('shutil.rmtree')
179
- def test_create_temp_directory(self, mock_rmtree, mock_makedirs, service):
180
- """Test temporary directory creation and cleanup"""
181
- correlation_id = "test-123"
182
-
183
- with service._create_temp_directory(correlation_id) as temp_dir:
184
- assert correlation_id in temp_dir
185
- mock_makedirs.assert_called_once()
186
-
187
- # Cleanup should be called
188
- mock_rmtree.assert_called_once()
189
-
190
- @patch('builtins.open', create=True)
191
- def test_convert_upload_to_audio_content_success(self, mock_open, service, sample_audio_upload):
192
- """Test successful audio upload conversion"""
193
- temp_dir = "/tmp/test"
194
- mock_file = MagicMock()
195
- mock_open.return_value.__enter__.return_value = mock_file
196
-
197
- result = service._convert_upload_to_audio_content(sample_audio_upload, temp_dir)
198
-
199
- assert isinstance(result, AudioContent)
200
- assert result.data == sample_audio_upload.content
201
- assert result.format == "wav"
202
- mock_file.write.assert_called_once_with(sample_audio_upload.content)
203
-
204
- @patch('builtins.open', side_effect=IOError("File error"))
205
- def test_convert_upload_to_audio_content_failure(self, mock_open, service, sample_audio_upload):
206
- """Test audio upload conversion failure"""
207
- temp_dir = "/tmp/test"
208
-
209
- with pytest.raises(AudioProcessingException, match="Failed to process uploaded audio"):
210
- service._convert_upload_to_audio_content(sample_audio_upload, temp_dir)
211
-
212
- def test_perform_speech_recognition_success(self, service, mock_container):
213
- """Test successful speech recognition"""
214
- audio = AudioContent(data=b"audio", format="wav", sample_rate=16000, duration=1.0)
215
- model = "whisper-small"
216
- correlation_id = "test-123"
217
-
218
- # Mock STT provider
219
- mock_stt_provider = Mock()
220
- expected_text = TextContent(text="Hello world", language="en")
221
- mock_stt_provider.transcribe.return_value = expected_text
222
- mock_container.get_stt_provider.return_value = mock_stt_provider
223
-
224
- result = service._perform_speech_recognition(audio, model, correlation_id)
225
-
226
- assert result == expected_text
227
- mock_container.get_stt_provider.assert_called_once_with(model)
228
- mock_stt_provider.transcribe.assert_called_once_with(audio, model)
229
-
230
- def test_perform_speech_recognition_failure(self, service, mock_container):
231
- """Test speech recognition failure"""
232
- audio = AudioContent(data=b"audio", format="wav", sample_rate=16000, duration=1.0)
233
- model = "whisper-small"
234
- correlation_id = "test-123"
235
-
236
- # Mock STT provider to raise exception
237
- mock_stt_provider = Mock()
238
- mock_stt_provider.transcribe.side_effect = Exception("STT failed")
239
- mock_container.get_stt_provider.return_value = mock_stt_provider
240
-
241
- with pytest.raises(SpeechRecognitionException, match="Speech recognition failed"):
242
- service._perform_speech_recognition(audio, model, correlation_id)
243
-
244
- def test_perform_translation_success(self, service, mock_container):
245
- """Test successful translation"""
246
- text = TextContent(text="Hello world", language="en")
247
- source_language = "en"
248
- target_language = "es"
249
- correlation_id = "test-123"
250
-
251
- # Mock translation provider
252
- mock_translation_provider = Mock()
253
- expected_text = TextContent(text="Hola mundo", language="es")
254
- mock_translation_provider.translate.return_value = expected_text
255
- mock_container.get_translation_provider.return_value = mock_translation_provider
256
-
257
- result = service._perform_translation(text, source_language, target_language, correlation_id)
258
-
259
- assert result == expected_text
260
- mock_container.get_translation_provider.assert_called_once()
261
- mock_translation_provider.translate.assert_called_once()
262
-
263
- def test_perform_translation_failure(self, service, mock_container):
264
- """Test translation failure"""
265
- text = TextContent(text="Hello world", language="en")
266
- source_language = "en"
267
- target_language = "es"
268
- correlation_id = "test-123"
269
-
270
- # Mock translation provider to raise exception
271
- mock_translation_provider = Mock()
272
- mock_translation_provider.translate.side_effect = Exception("Translation failed")
273
- mock_container.get_translation_provider.return_value = mock_translation_provider
274
-
275
- with pytest.raises(TranslationFailedException, match="Translation failed"):
276
- service._perform_translation(text, source_language, target_language, correlation_id)
277
-
278
- @patch('builtins.open', create=True)
279
- def test_perform_speech_synthesis_success(self, mock_open, service, mock_container):
280
- """Test successful speech synthesis"""
281
- text = TextContent(text="Hola mundo", language="es")
282
- voice = "chatterbox"
283
- speed = 1.0
284
- language = "es"
285
- temp_dir = "/tmp/test"
286
- correlation_id = "test-123"
287
-
288
- # Mock TTS provider
289
- mock_tts_provider = Mock()
290
- mock_audio = AudioContent(data=b"synthesized_audio", format="wav", sample_rate=22050, duration=2.0)
291
- mock_tts_provider.synthesize.return_value = mock_audio
292
- mock_container.get_tts_provider.return_value = mock_tts_provider
293
-
294
- # Mock file operations
295
- mock_file = MagicMock()
296
- mock_open.return_value.__enter__.return_value = mock_file
297
-
298
- result = service._perform_speech_synthesis(text, voice, speed, language, temp_dir, correlation_id)
299
-
300
- assert correlation_id in result
301
- assert result.endswith(".wav")
302
- mock_container.get_tts_provider.assert_called_once_with(voice)
303
- mock_tts_provider.synthesize.assert_called_once()
304
- mock_file.write.assert_called_once_with(mock_audio.data)
305
-
306
- def test_perform_speech_synthesis_failure(self, service, mock_container):
307
- """Test speech synthesis failure"""
308
- text = TextContent(text="Hola mundo", language="es")
309
- voice = "chatterbox"
310
- speed = 1.0
311
- language = "es"
312
- temp_dir = "/tmp/test"
313
- correlation_id = "test-123"
314
-
315
- # Mock TTS provider to raise exception
316
- mock_tts_provider = Mock()
317
- mock_tts_provider.synthesize.side_effect = Exception("TTS failed")
318
- mock_container.get_tts_provider.return_value = mock_tts_provider
319
-
320
- with pytest.raises(SpeechSynthesisException, match="Speech synthesis failed"):
321
- service._perform_speech_synthesis(text, voice, speed, language, temp_dir, correlation_id)
322
-
323
- def test_get_error_code_from_exception(self, service):
324
- """Test error code mapping from exceptions"""
325
- assert service._get_error_code_from_exception(SpeechRecognitionException("test")) == 'STT_ERROR'
326
- assert service._get_error_code_from_exception(TranslationFailedException("test")) == 'TRANSLATION_ERROR'
327
- assert service._get_error_code_from_exception(SpeechSynthesisException("test")) == 'TTS_ERROR'
328
- assert service._get_error_code_from_exception(ValueError("test")) == 'VALIDATION_ERROR'
329
- assert service._get_error_code_from_exception(Exception("test")) == 'SYSTEM_ERROR'
330
-
331
- @patch('os.path.exists')
332
- @patch('os.remove')
333
- def test_cleanup_temp_files_success(self, mock_remove, mock_exists, service):
334
- """Test successful temporary file cleanup"""
335
- service._temp_files = {
336
- "/tmp/file1.wav": "/tmp/file1.wav",
337
- "/tmp/file2.wav": "/tmp/file2.wav"
338
- }
339
- mock_exists.return_value = True
340
-
341
- service._cleanup_temp_files()
342
-
343
- assert service._temp_files == {}
344
- assert mock_remove.call_count == 2
345
-
346
- @patch('os.path.exists')
347
- @patch('os.remove', side_effect=OSError("Permission denied"))
348
- def test_cleanup_temp_files_failure(self, mock_remove, mock_exists, service):
349
- """Test temporary file cleanup with failures"""
350
- service._temp_files = {"/tmp/file1.wav": "/tmp/file1.wav"}
351
- mock_exists.return_value = True
352
-
353
- # Should not raise exception
354
- service._cleanup_temp_files()
355
-
356
- # File should still be removed from tracking
357
- assert service._temp_files == {}
358
-
359
- def test_get_processing_status(self, service):
360
- """Test processing status retrieval"""
361
- correlation_id = "test-123"
362
-
363
- result = service.get_processing_status(correlation_id)
364
-
365
- assert result['correlation_id'] == correlation_id
366
- assert 'status' in result
367
- assert 'message' in result
368
-
369
- def test_get_supported_configurations(self, service):
370
- """Test supported configurations retrieval"""
371
- result = service.get_supported_configurations()
372
-
373
- assert 'asr_models' in result
374
- assert 'voices' in result
375
- assert 'languages' in result
376
- assert 'audio_formats' in result
377
- assert 'max_file_size_mb' in result
378
- assert 'speed_range' in result
379
-
380
- # Verify expected values
381
- assert 'whisper-small' in result['asr_models']
382
- assert 'chatterbox' in result['voices']
383
- assert 'en' in result['languages']
384
-
385
- def test_cleanup(self, service):
386
- """Test service cleanup"""
387
- service._temp_files = {"/tmp/test.wav": "/tmp/test.wav"}
388
-
389
- with patch.object(service, '_cleanup_temp_files') as mock_cleanup:
390
- service.cleanup()
391
- mock_cleanup.assert_called_once()
392
-
393
- def test_context_manager(self, service):
394
- """Test service as context manager"""
395
- with patch.object(service, 'cleanup') as mock_cleanup:
396
- with service as svc:
397
- assert svc == service
398
- mock_cleanup.assert_called_once()
399
-
400
- @patch('src.application.services.audio_processing_service.time.time')
401
- @patch.object(AudioProcessingApplicationService, '_create_temp_directory')
402
- @patch.object(AudioProcessingApplicationService, '_convert_upload_to_audio_content')
403
- @patch.object(AudioProcessingApplicationService, '_perform_speech_recognition_with_recovery')
404
- @patch.object(AudioProcessingApplicationService, '_perform_translation_with_recovery')
405
- @patch.object(AudioProcessingApplicationService, '_perform_speech_synthesis_with_recovery')
406
- def test_process_audio_pipeline_success(self, mock_tts, mock_translation, mock_stt,
407
- mock_convert, mock_temp_dir, mock_time,
408
- service, sample_processing_request):
409
- """Test successful audio processing pipeline"""
410
- # Setup mocks
411
- mock_time.side_effect = [0.0, 5.0] # Start and end times
412
- mock_temp_dir.return_value.__enter__.return_value = "/tmp/test"
413
- mock_temp_dir.return_value.__exit__.return_value = None
414
-
415
- mock_audio = AudioContent(data=b"audio", format="wav", sample_rate=16000, duration=1.0)
416
- mock_convert.return_value = mock_audio
417
-
418
- mock_original_text = TextContent(text="Hello world", language="en")
419
- mock_stt.return_value = mock_original_text
420
-
421
- mock_translated_text = TextContent(text="Hola mundo", language="es")
422
- mock_translation.return_value = mock_translated_text
423
-
424
- mock_tts.return_value = "/tmp/test/output_123.wav"
425
-
426
- with patch.object(service, '_validate_request'):
427
- result = service.process_audio_pipeline(sample_processing_request)
428
-
429
- # Verify result
430
- assert isinstance(result, ProcessingResultDto)
431
- assert result.success is True
432
- assert result.original_text == "Hello world"
433
- assert result.translated_text == "Hola mundo"
434
- assert result.audio_path == "/tmp/test/output_123.wav"
435
- assert result.processing_time == 5.0
436
-
437
- @patch('src.application.services.audio_processing_service.time.time')
438
- def test_process_audio_pipeline_validation_error(self, mock_time, service, sample_processing_request):
439
- """Test audio processing pipeline with validation error"""
440
- mock_time.side_effect = [0.0, 1.0]
441
-
442
- with patch.object(service, '_validate_request', side_effect=ValueError("Invalid request")):
443
- result = service.process_audio_pipeline(sample_processing_request)
444
-
445
- # Verify error result
446
- assert isinstance(result, ProcessingResultDto)
447
- assert result.success is False
448
- assert "Invalid request" in result.error_message
449
- assert result.processing_time == 1.0
450
-
451
- @patch('src.application.services.audio_processing_service.time.time')
452
- def test_process_audio_pipeline_domain_exception(self, mock_time, service, sample_processing_request):
453
- """Test audio processing pipeline with domain exception"""
454
- mock_time.side_effect = [0.0, 2.0]
455
-
456
- with patch.object(service, '_validate_request'):
457
- with patch.object(service, '_create_temp_directory', side_effect=SpeechRecognitionException("STT failed")):
458
- result = service.process_audio_pipeline(sample_processing_request)
459
-
460
- # Verify error result
461
- assert isinstance(result, ProcessingResultDto)
462
- assert result.success is False
463
- assert result.error_message is not None
464
- assert result.processing_time == 2.0
465
-
466
- def test_recovery_methods_exist(self, service):
467
- """Test that recovery methods exist and are callable"""
468
- # These methods should exist for error recovery
469
- assert hasattr(service, '_perform_speech_recognition_with_recovery')
470
- assert hasattr(service, '_perform_translation_with_recovery')
471
- assert hasattr(service, '_perform_speech_synthesis_with_recovery')
472
-
473
- assert callable(service._perform_speech_recognition_with_recovery)
474
- assert callable(service._perform_translation_with_recovery)
475
- assert callable(service._perform_speech_synthesis_with_recovery)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/application/services/test_configuration_service.py DELETED
@@ -1,572 +0,0 @@
1
- """Unit tests for ConfigurationApplicationService"""
2
-
3
- import pytest
4
- import os
5
- import json
6
- from unittest.mock import Mock, MagicMock, patch, mock_open
7
-
8
- from src.application.services.configuration_service import (
9
- ConfigurationApplicationService,
10
- ConfigurationException
11
- )
12
- from src.infrastructure.config.app_config import AppConfig
13
- from src.infrastructure.config.dependency_container import DependencyContainer
14
-
15
-
16
- class TestConfigurationApplicationService:
17
- """Test cases for ConfigurationApplicationService"""
18
-
19
- @pytest.fixture
20
- def mock_container(self):
21
- """Create mock dependency container"""
22
- container = Mock(spec=DependencyContainer)
23
- return container
24
-
25
- @pytest.fixture
26
- def mock_config(self):
27
- """Create mock application config"""
28
- config = Mock(spec=AppConfig)
29
-
30
- # Mock configuration methods
31
- config.get_tts_config.return_value = {
32
- 'preferred_providers': ['chatterbox'],
33
- 'default_speed': 1.0,
34
- 'default_language': 'en',
35
- 'enable_streaming': False,
36
- 'max_text_length': 5000
37
- }
38
-
39
- config.get_stt_config.return_value = {
40
- 'preferred_providers': ['whisper', 'parakeet'],
41
- 'default_model': 'whisper',
42
- 'chunk_length_s': 30,
43
- 'batch_size': 16,
44
- 'enable_vad': True
45
- }
46
-
47
- config.get_translation_config.return_value = {
48
- 'default_provider': 'nllb',
49
- 'model_name': 'nllb-200-3.3B',
50
- 'max_chunk_length': 512,
51
- 'batch_size': 8,
52
- 'cache_translations': True
53
- }
54
-
55
- config.get_processing_config.return_value = {
56
- 'temp_dir': '/tmp',
57
- 'cleanup_temp_files': True,
58
- 'max_file_size_mb': 100,
59
- 'supported_audio_formats': ['wav', 'mp3', 'flac'],
60
- 'processing_timeout_seconds': 300
61
- }
62
-
63
- config.get_logging_config.return_value = {
64
- 'level': 'INFO',
65
- 'enable_file_logging': False,
66
- 'log_file_path': '/tmp/app.log',
67
- 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
68
- }
69
-
70
- # Mock config objects for attribute access
71
- config.tts = Mock()
72
- config.stt = Mock()
73
- config.translation = Mock()
74
- config.processing = Mock()
75
- config.logging = Mock()
76
-
77
- return config
78
-
79
- @pytest.fixture
80
- def service(self, mock_container, mock_config):
81
- """Create ConfigurationApplicationService instance"""
82
- mock_container.resolve.return_value = mock_config
83
- return ConfigurationApplicationService(mock_container, mock_config)
84
-
85
- def test_initialization(self, mock_container, mock_config):
86
- """Test service initialization"""
87
- service = ConfigurationApplicationService(mock_container, mock_config)
88
-
89
- assert service._container == mock_container
90
- assert service._config == mock_config
91
- assert service._error_mapper is not None
92
-
93
- def test_initialization_without_config(self, mock_container, mock_config):
94
- """Test service initialization without explicit config"""
95
- mock_container.resolve.return_value = mock_config
96
-
97
- service = ConfigurationApplicationService(mock_container)
98
-
99
- assert service._container == mock_container
100
- assert service._config == mock_config
101
- mock_container.resolve.assert_called_once_with(AppConfig)
102
-
103
- def test_get_current_configuration_success(self, service, mock_config):
104
- """Test successful current configuration retrieval"""
105
- result = service.get_current_configuration()
106
-
107
- assert 'tts' in result
108
- assert 'stt' in result
109
- assert 'translation' in result
110
- assert 'processing' in result
111
- assert 'logging' in result
112
-
113
- # Verify all config methods were called
114
- mock_config.get_tts_config.assert_called_once()
115
- mock_config.get_stt_config.assert_called_once()
116
- mock_config.get_translation_config.assert_called_once()
117
- mock_config.get_processing_config.assert_called_once()
118
- mock_config.get_logging_config.assert_called_once()
119
-
120
- def test_get_current_configuration_failure(self, service, mock_config):
121
- """Test current configuration retrieval failure"""
122
- mock_config.get_tts_config.side_effect = Exception("Config error")
123
-
124
- with pytest.raises(ConfigurationException, match="Failed to retrieve configuration"):
125
- service.get_current_configuration()
126
-
127
- def test_get_tts_configuration_success(self, service, mock_config):
128
- """Test successful TTS configuration retrieval"""
129
- result = service.get_tts_configuration()
130
-
131
- assert result['preferred_providers'] == ['chatterbox']
132
- assert result['default_speed'] == 1.0
133
- mock_config.get_tts_config.assert_called_once()
134
-
135
- def test_get_tts_configuration_failure(self, service, mock_config):
136
- """Test TTS configuration retrieval failure"""
137
- mock_config.get_tts_config.side_effect = Exception("TTS config error")
138
-
139
- with pytest.raises(ConfigurationException, match="Failed to retrieve TTS configuration"):
140
- service.get_tts_configuration()
141
-
142
- def test_get_stt_configuration_success(self, service, mock_config):
143
- """Test successful STT configuration retrieval"""
144
- result = service.get_stt_configuration()
145
-
146
- assert result['preferred_providers'] == ['whisper', 'parakeet']
147
- assert result['default_model'] == 'whisper'
148
- mock_config.get_stt_config.assert_called_once()
149
-
150
- def test_get_stt_configuration_failure(self, service, mock_config):
151
- """Test STT configuration retrieval failure"""
152
- mock_config.get_stt_config.side_effect = Exception("STT config error")
153
-
154
- with pytest.raises(ConfigurationException, match="Failed to retrieve STT configuration"):
155
- service.get_stt_configuration()
156
-
157
- def test_get_translation_configuration_success(self, service, mock_config):
158
- """Test successful translation configuration retrieval"""
159
- result = service.get_translation_configuration()
160
-
161
- assert result['default_provider'] == 'nllb'
162
- assert result['model_name'] == 'nllb-200-3.3B'
163
- mock_config.get_translation_config.assert_called_once()
164
-
165
- def test_get_translation_configuration_failure(self, service, mock_config):
166
- """Test translation configuration retrieval failure"""
167
- mock_config.get_translation_config.side_effect = Exception("Translation config error")
168
-
169
- with pytest.raises(ConfigurationException, match="Failed to retrieve translation configuration"):
170
- service.get_translation_configuration()
171
-
172
- def test_get_processing_configuration_success(self, service, mock_config):
173
- """Test successful processing configuration retrieval"""
174
- result = service.get_processing_configuration()
175
-
176
- assert result['temp_dir'] == '/tmp'
177
- assert result['max_file_size_mb'] == 100
178
- mock_config.get_processing_config.assert_called_once()
179
-
180
- def test_get_processing_configuration_failure(self, service, mock_config):
181
- """Test processing configuration retrieval failure"""
182
- mock_config.get_processing_config.side_effect = Exception("Processing config error")
183
-
184
- with pytest.raises(ConfigurationException, match="Failed to retrieve processing configuration"):
185
- service.get_processing_configuration()
186
-
187
- def test_get_logging_configuration_success(self, service, mock_config):
188
- """Test successful logging configuration retrieval"""
189
- result = service.get_logging_configuration()
190
-
191
- assert result['level'] == 'INFO'
192
- assert result['enable_file_logging'] is False
193
- mock_config.get_logging_config.assert_called_once()
194
-
195
- def test_get_logging_configuration_failure(self, service, mock_config):
196
- """Test logging configuration retrieval failure"""
197
- mock_config.get_logging_config.side_effect = Exception("Logging config error")
198
-
199
- with pytest.raises(ConfigurationException, match="Failed to retrieve logging configuration"):
200
- service.get_logging_configuration()
201
-
202
- def test_update_tts_configuration_success(self, service, mock_config):
203
- """Test successful TTS configuration update"""
204
- updates = {
205
- 'default_speed': 1.5,
206
- 'enable_streaming': True
207
- }
208
-
209
- result = service.update_tts_configuration(updates)
210
-
211
- # Verify setattr was called for valid attributes
212
- assert hasattr(mock_config.tts, 'default_speed')
213
- assert hasattr(mock_config.tts, 'enable_streaming')
214
-
215
- # Verify updated config was returned
216
- mock_config.get_tts_config.assert_called()
217
-
218
- def test_update_tts_configuration_validation_error(self, service):
219
- """Test TTS configuration update with validation error"""
220
- updates = {
221
- 'default_speed': 5.0 # Invalid speed > 3.0
222
- }
223
-
224
- with pytest.raises(ConfigurationException, match="default_speed must be between 0.1 and 3.0"):
225
- service.update_tts_configuration(updates)
226
-
227
- def test_update_stt_configuration_success(self, service, mock_config):
228
- """Test successful STT configuration update"""
229
- updates = {
230
- 'chunk_length_s': 60,
231
- 'enable_vad': False
232
- }
233
-
234
- result = service.update_stt_configuration(updates)
235
-
236
- # Verify setattr was called for valid attributes
237
- assert hasattr(mock_config.stt, 'chunk_length_s')
238
- assert hasattr(mock_config.stt, 'enable_vad')
239
-
240
- # Verify updated config was returned
241
- mock_config.get_stt_config.assert_called()
242
-
243
- def test_update_stt_configuration_validation_error(self, service):
244
- """Test STT configuration update with validation error"""
245
- updates = {
246
- 'chunk_length_s': -10 # Invalid negative value
247
- }
248
-
249
- with pytest.raises(ConfigurationException, match="chunk_length_s must be a positive integer"):
250
- service.update_stt_configuration(updates)
251
-
252
- def test_update_translation_configuration_success(self, service, mock_config):
253
- """Test successful translation configuration update"""
254
- updates = {
255
- 'max_chunk_length': 1024,
256
- 'cache_translations': False
257
- }
258
-
259
- result = service.update_translation_configuration(updates)
260
-
261
- # Verify setattr was called for valid attributes
262
- assert hasattr(mock_config.translation, 'max_chunk_length')
263
- assert hasattr(mock_config.translation, 'cache_translations')
264
-
265
- # Verify updated config was returned
266
- mock_config.get_translation_config.assert_called()
267
-
268
- def test_update_translation_configuration_validation_error(self, service):
269
- """Test translation configuration update with validation error"""
270
- updates = {
271
- 'max_chunk_length': 0 # Invalid zero value
272
- }
273
-
274
- with pytest.raises(ConfigurationException, match="max_chunk_length must be a positive integer"):
275
- service.update_translation_configuration(updates)
276
-
277
- def test_update_processing_configuration_success(self, service, mock_config):
278
- """Test successful processing configuration update"""
279
- updates = {
280
- 'max_file_size_mb': 200,
281
- 'cleanup_temp_files': False
282
- }
283
-
284
- with patch('pathlib.Path.mkdir'):
285
- result = service.update_processing_configuration(updates)
286
-
287
- # Verify setattr was called for valid attributes
288
- assert hasattr(mock_config.processing, 'max_file_size_mb')
289
- assert hasattr(mock_config.processing, 'cleanup_temp_files')
290
-
291
- # Verify updated config was returned
292
- mock_config.get_processing_config.assert_called()
293
-
294
- def test_update_processing_configuration_validation_error(self, service):
295
- """Test processing configuration update with validation error"""
296
- updates = {
297
- 'max_file_size_mb': -50 # Invalid negative value
298
- }
299
-
300
- with pytest.raises(ConfigurationException, match="max_file_size_mb must be a positive integer"):
301
- service.update_processing_configuration(updates)
302
-
303
- def test_validate_tts_updates_valid(self, service):
304
- """Test TTS updates validation with valid data"""
305
- updates = {
306
- 'preferred_providers': ['chatterbox'],
307
- 'default_speed': 1.5,
308
- 'default_language': 'es',
309
- 'enable_streaming': True,
310
- 'max_text_length': 10000
311
- }
312
-
313
- # Should not raise exception
314
- service._validate_tts_updates(updates)
315
-
316
- def test_validate_tts_updates_invalid_provider(self, service):
317
- """Test TTS updates validation with invalid provider"""
318
- updates = {
319
- 'preferred_providers': ['invalid_provider']
320
- }
321
-
322
- with pytest.raises(ConfigurationException, match="Invalid TTS provider"):
323
- service._validate_tts_updates(updates)
324
-
325
- def test_validate_tts_updates_invalid_speed(self, service):
326
- """Test TTS updates validation with invalid speed"""
327
- updates = {
328
- 'default_speed': 5.0 # Too high
329
- }
330
-
331
- with pytest.raises(ConfigurationException, match="default_speed must be between 0.1 and 3.0"):
332
- service._validate_tts_updates(updates)
333
-
334
- def test_validate_stt_updates_valid(self, service):
335
- """Test STT updates validation with valid data"""
336
- updates = {
337
- 'preferred_providers': ['whisper', 'parakeet'],
338
- 'default_model': 'whisper',
339
- 'chunk_length_s': 45,
340
- 'batch_size': 32,
341
- 'enable_vad': False
342
- }
343
-
344
- # Should not raise exception
345
- service._validate_stt_updates(updates)
346
-
347
- def test_validate_stt_updates_invalid_provider(self, service):
348
- """Test STT updates validation with invalid provider"""
349
- updates = {
350
- 'preferred_providers': ['invalid_stt']
351
- }
352
-
353
- with pytest.raises(ConfigurationException, match="Invalid STT provider"):
354
- service._validate_stt_updates(updates)
355
-
356
- def test_validate_translation_updates_valid(self, service):
357
- """Test translation updates validation with valid data"""
358
- updates = {
359
- 'default_provider': 'nllb',
360
- 'model_name': 'nllb-200-1.3B',
361
- 'max_chunk_length': 256,
362
- 'batch_size': 4,
363
- 'cache_translations': False
364
- }
365
-
366
- # Should not raise exception
367
- service._validate_translation_updates(updates)
368
-
369
- def test_validate_processing_updates_valid(self, service):
370
- """Test processing updates validation with valid data"""
371
- updates = {
372
- 'temp_dir': '/tmp/test',
373
- 'cleanup_temp_files': True,
374
- 'max_file_size_mb': 150,
375
- 'supported_audio_formats': ['wav', 'mp3'],
376
- 'processing_timeout_seconds': 600
377
- }
378
-
379
- with patch('pathlib.Path.mkdir'):
380
- # Should not raise exception
381
- service._validate_processing_updates(updates)
382
-
383
- def test_validate_processing_updates_invalid_format(self, service):
384
- """Test processing updates validation with invalid audio format"""
385
- updates = {
386
- 'supported_audio_formats': ['wav', 'invalid_format']
387
- }
388
-
389
- with pytest.raises(ConfigurationException, match="Invalid audio format"):
390
- service._validate_processing_updates(updates)
391
-
392
- def test_save_configuration_to_file_success(self, service, mock_config):
393
- """Test successful configuration save to file"""
394
- file_path = "/tmp/config.json"
395
-
396
- service.save_configuration_to_file(file_path)
397
-
398
- mock_config.save_configuration.assert_called_once_with(file_path)
399
-
400
- def test_save_configuration_to_file_failure(self, service, mock_config):
401
- """Test configuration save to file failure"""
402
- file_path = "/tmp/config.json"
403
- mock_config.save_configuration.side_effect = Exception("Save failed")
404
-
405
- with pytest.raises(ConfigurationException, match="Failed to save configuration"):
406
- service.save_configuration_to_file(file_path)
407
-
408
- @patch('os.path.exists')
409
- def test_load_configuration_from_file_success(self, mock_exists, service, mock_container):
410
- """Test successful configuration load from file"""
411
- file_path = "/tmp/config.json"
412
- mock_exists.return_value = True
413
-
414
- with patch('src.infrastructure.config.app_config.AppConfig') as mock_app_config:
415
- new_config = Mock()
416
- mock_app_config.return_value = new_config
417
-
418
- result = service.load_configuration_from_file(file_path)
419
-
420
- # Verify new config was created and registered
421
- mock_app_config.assert_called_once_with(config_file=file_path)
422
- mock_container.register_singleton.assert_called_once_with(AppConfig, new_config)
423
-
424
- @patch('os.path.exists')
425
- def test_load_configuration_from_file_not_found(self, mock_exists, service):
426
- """Test configuration load from non-existent file"""
427
- file_path = "/tmp/nonexistent.json"
428
- mock_exists.return_value = False
429
-
430
- with pytest.raises(ConfigurationException, match="Configuration file not found"):
431
- service.load_configuration_from_file(file_path)
432
-
433
- def test_reload_configuration_success(self, service, mock_config):
434
- """Test successful configuration reload"""
435
- result = service.reload_configuration()
436
-
437
- mock_config.reload_configuration.assert_called_once()
438
- assert 'tts' in result
439
-
440
- def test_reload_configuration_failure(self, service, mock_config):
441
- """Test configuration reload failure"""
442
- mock_config.reload_configuration.side_effect = Exception("Reload failed")
443
-
444
- with pytest.raises(ConfigurationException, match="Failed to reload configuration"):
445
- service.reload_configuration()
446
-
447
- def test_get_provider_availability(self, service, mock_container):
448
- """Test provider availability check"""
449
- # Mock factories
450
- mock_tts_factory = Mock()
451
- mock_stt_factory = Mock()
452
- mock_translation_factory = Mock()
453
-
454
- mock_container.resolve.side_effect = [mock_tts_factory, mock_stt_factory, mock_translation_factory]
455
-
456
- # Mock successful provider creation
457
- mock_tts_factory.create_provider.return_value = Mock()
458
- mock_stt_factory.create_provider.return_value = Mock()
459
- mock_translation_factory.get_default_provider.return_value = Mock()
460
-
461
- result = service.get_provider_availability()
462
-
463
- assert 'tts' in result
464
- assert 'stt' in result
465
- assert 'translation' in result
466
-
467
- # All providers should be available
468
- assert all(result['tts'].values())
469
- assert all(result['stt'].values())
470
- assert result['translation']['nllb'] is True
471
-
472
- def test_get_system_info(self, service, mock_config):
473
- """Test system information retrieval"""
474
- mock_config.config_file = "/tmp/config.json"
475
- mock_config.processing.temp_dir = "/tmp"
476
- mock_config.logging.level = "INFO"
477
- mock_config.processing.supported_audio_formats = ['wav', 'mp3']
478
- mock_config.processing.max_file_size_mb = 100
479
- mock_config.processing.processing_timeout_seconds = 300
480
-
481
- with patch.object(service, 'get_provider_availability', return_value={}):
482
- result = service.get_system_info()
483
-
484
- assert result['config_file'] == "/tmp/config.json"
485
- assert result['temp_directory'] == "/tmp"
486
- assert result['log_level'] == "INFO"
487
- assert 'supported_languages' in result
488
- assert 'supported_audio_formats' in result
489
- assert 'max_file_size_mb' in result
490
-
491
- def test_validate_configuration_success(self, service, mock_config):
492
- """Test successful configuration validation"""
493
- # Mock valid configuration
494
- mock_config.get_tts_config.return_value = {
495
- 'default_speed': 1.0,
496
- 'max_text_length': 5000
497
- }
498
- mock_config.get_stt_config.return_value = {
499
- 'chunk_length_s': 30,
500
- 'batch_size': 16
501
- }
502
- mock_config.get_processing_config.return_value = {
503
- 'temp_dir': '/tmp',
504
- 'max_file_size_mb': 100
505
- }
506
- mock_config.get_logging_config.return_value = {
507
- 'level': 'INFO'
508
- }
509
-
510
- with patch('os.path.exists', return_value=True):
511
- result = service.validate_configuration()
512
-
513
- # Should have no issues
514
- assert all(len(issues) == 0 for issues in result.values())
515
-
516
- def test_validate_configuration_with_issues(self, service, mock_config):
517
- """Test configuration validation with issues"""
518
- # Mock invalid configuration
519
- mock_config.get_tts_config.return_value = {
520
- 'default_speed': 5.0, # Invalid
521
- 'max_text_length': -100 # Invalid
522
- }
523
- mock_config.get_stt_config.return_value = {
524
- 'chunk_length_s': -10, # Invalid
525
- 'batch_size': 0 # Invalid
526
- }
527
- mock_config.get_processing_config.return_value = {
528
- 'temp_dir': '/nonexistent', # Invalid
529
- 'max_file_size_mb': -50 # Invalid
530
- }
531
- mock_config.get_logging_config.return_value = {
532
- 'level': 'INVALID' # Invalid
533
- }
534
-
535
- with patch('os.path.exists', return_value=False):
536
- result = service.validate_configuration()
537
-
538
- # Should have issues in each category
539
- assert len(result['tts']) > 0
540
- assert len(result['stt']) > 0
541
- assert len(result['processing']) > 0
542
- assert len(result['logging']) > 0
543
-
544
- def test_reset_to_defaults_success(self, service, mock_container):
545
- """Test successful configuration reset to defaults"""
546
- with patch('src.infrastructure.config.app_config.AppConfig') as mock_app_config:
547
- default_config = Mock()
548
- mock_app_config.return_value = default_config
549
-
550
- result = service.reset_to_defaults()
551
-
552
- # Verify new default config was created and registered
553
- mock_app_config.assert_called_once_with()
554
- mock_container.register_singleton.assert_called_once_with(AppConfig, default_config)
555
-
556
- def test_reset_to_defaults_failure(self, service):
557
- """Test configuration reset to defaults failure"""
558
- with patch('src.infrastructure.config.app_config.AppConfig', side_effect=Exception("Reset failed")):
559
- with pytest.raises(ConfigurationException, match="Failed to reset configuration"):
560
- service.reset_to_defaults()
561
-
562
- def test_cleanup(self, service):
563
- """Test service cleanup"""
564
- # Should not raise exception
565
- service.cleanup()
566
-
567
- def test_context_manager(self, service):
568
- """Test service as context manager"""
569
- with patch.object(service, 'cleanup') as mock_cleanup:
570
- with service as svc:
571
- assert svc == service
572
- mock_cleanup.assert_called_once()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/interfaces/__init__.py DELETED
@@ -1 +0,0 @@
1
- # Domain interface tests
 
 
tests/unit/domain/interfaces/test_audio_processing.py DELETED
@@ -1,212 +0,0 @@
1
- """Unit tests for IAudioProcessingService interface contract."""
2
-
3
- import pytest
4
- from abc import ABC
5
- from unittest.mock import Mock
6
- from src.domain.interfaces.audio_processing import IAudioProcessingService
7
- from src.domain.models.audio_content import AudioContent
8
- from src.domain.models.voice_settings import VoiceSettings
9
- from src.domain.models.processing_result import ProcessingResult
10
- from src.domain.models.text_content import TextContent
11
-
12
-
13
- class TestIAudioProcessingService:
14
- """Test cases for IAudioProcessingService interface contract."""
15
-
16
- def test_interface_is_abstract(self):
17
- """Test that IAudioProcessingService is an abstract base class."""
18
- assert issubclass(IAudioProcessingService, ABC)
19
-
20
- # Should not be able to instantiate directly
21
- with pytest.raises(TypeError):
22
- IAudioProcessingService() # type: ignore
23
-
24
- def test_interface_has_required_method(self):
25
- """Test that interface defines the required abstract method."""
26
- # Check that the method exists and is abstract
27
- assert hasattr(IAudioProcessingService, 'process_audio_pipeline')
28
- assert getattr(IAudioProcessingService.process_audio_pipeline, '__isabstractmethod__', False)
29
-
30
- def test_method_signature(self):
31
- """Test that the method has the correct signature."""
32
- import inspect
33
-
34
- method = IAudioProcessingService.process_audio_pipeline
35
- signature = inspect.signature(method)
36
-
37
- # Check parameter names and types
38
- params = list(signature.parameters.keys())
39
- expected_params = ['self', 'audio', 'target_language', 'voice_settings']
40
-
41
- assert params == expected_params
42
-
43
- # Check return annotation
44
- assert signature.return_annotation == "'ProcessingResult'"
45
-
46
- def test_concrete_implementation_must_implement_method(self):
47
- """Test that concrete implementations must implement the abstract method."""
48
-
49
- class IncompleteImplementation(IAudioProcessingService):
50
- pass
51
-
52
- # Should not be able to instantiate without implementing abstract method
53
- with pytest.raises(TypeError, match="Can't instantiate abstract class"):
54
- IncompleteImplementation() # type: ignore
55
-
56
- def test_concrete_implementation_with_method(self):
57
- """Test that concrete implementation with method can be instantiated."""
58
-
59
- class ConcreteImplementation(IAudioProcessingService):
60
- def process_audio_pipeline(self, audio, target_language, voice_settings):
61
- return ProcessingResult.success_result(
62
- original_text=TextContent(text="test", language="en")
63
- )
64
-
65
- # Should be able to instantiate
66
- implementation = ConcreteImplementation()
67
- assert isinstance(implementation, IAudioProcessingService)
68
-
69
- def test_method_contract_with_mock(self):
70
- """Test the method contract using a mock implementation."""
71
-
72
- class MockImplementation(IAudioProcessingService):
73
- def __init__(self):
74
- self.mock_method = Mock()
75
-
76
- def process_audio_pipeline(self, audio, target_language, voice_settings):
77
- return self.mock_method(audio, target_language, voice_settings)
78
-
79
- # Create test data
80
- audio = AudioContent(
81
- data=b"test_audio",
82
- format="wav",
83
- sample_rate=22050,
84
- duration=5.0
85
- )
86
- voice_settings = VoiceSettings(
87
- voice_id="test_voice",
88
- speed=1.0,
89
- language="es"
90
- )
91
- expected_result = ProcessingResult.success_result(
92
- original_text=TextContent(text="test", language="en")
93
- )
94
-
95
- # Setup mock
96
- implementation = MockImplementation()
97
- implementation.mock_method.return_value = expected_result
98
-
99
- # Call method
100
- result = implementation.process_audio_pipeline(
101
- audio=audio,
102
- target_language="es",
103
- voice_settings=voice_settings
104
- )
105
-
106
- # Verify call and result
107
- implementation.mock_method.assert_called_once_with(audio, "es", voice_settings)
108
- assert result == expected_result
109
-
110
- def test_interface_docstring_requirements(self):
111
- """Test that the interface method has proper documentation."""
112
- method = IAudioProcessingService.process_audio_pipeline
113
-
114
- assert method.__doc__ is not None
115
- docstring = method.__doc__
116
-
117
- # Check that docstring contains key information
118
- assert "Process audio through the complete pipeline" in docstring
119
- assert "STT -> Translation -> TTS" in docstring
120
- assert "Args:" in docstring
121
- assert "Returns:" in docstring
122
- assert "Raises:" in docstring
123
- assert "AudioProcessingException" in docstring
124
-
125
- def test_interface_type_hints(self):
126
- """Test that the interface uses proper type hints."""
127
- import inspect
128
- from typing import get_type_hints
129
-
130
- # Get type hints (this will resolve string annotations)
131
- try:
132
- hints = get_type_hints(IAudioProcessingService.process_audio_pipeline)
133
- except NameError:
134
- # If forward references can't be resolved, check annotations directly
135
- method = IAudioProcessingService.process_audio_pipeline
136
- annotations = getattr(method, '__annotations__', {})
137
-
138
- assert 'audio' in annotations
139
- assert 'target_language' in annotations
140
- assert 'voice_settings' in annotations
141
- assert 'return' in annotations
142
-
143
- # Check that type annotations are strings (forward references)
144
- assert annotations['audio'] == "'AudioContent'"
145
- assert annotations['target_language'] == str
146
- assert annotations['voice_settings'] == "'VoiceSettings'"
147
- assert annotations['return'] == "'ProcessingResult'"
148
-
149
- def test_multiple_implementations_possible(self):
150
- """Test that multiple implementations of the interface are possible."""
151
-
152
- class Implementation1(IAudioProcessingService):
153
- def process_audio_pipeline(self, audio, target_language, voice_settings):
154
- return ProcessingResult.success_result(
155
- original_text=TextContent(text="impl1", language="en")
156
- )
157
-
158
- class Implementation2(IAudioProcessingService):
159
- def process_audio_pipeline(self, audio, target_language, voice_settings):
160
- return ProcessingResult.failure_result(error_message="impl2 failed")
161
-
162
- impl1 = Implementation1()
163
- impl2 = Implementation2()
164
-
165
- assert isinstance(impl1, IAudioProcessingService)
166
- assert isinstance(impl2, IAudioProcessingService)
167
- assert type(impl1) != type(impl2)
168
-
169
- def test_interface_method_can_be_called_polymorphically(self):
170
- """Test that interface methods can be called polymorphically."""
171
-
172
- class TestImplementation(IAudioProcessingService):
173
- def __init__(self, result):
174
- self.result = result
175
-
176
- def process_audio_pipeline(self, audio, target_language, voice_settings):
177
- return self.result
178
-
179
- # Create different implementations
180
- success_result = ProcessingResult.success_result(
181
- original_text=TextContent(text="success", language="en")
182
- )
183
- failure_result = ProcessingResult.failure_result(error_message="failed")
184
-
185
- implementations = [
186
- TestImplementation(success_result),
187
- TestImplementation(failure_result)
188
- ]
189
-
190
- # Test polymorphic usage
191
- audio = AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0)
192
- voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en")
193
-
194
- results = []
195
- for impl in implementations:
196
- # Can call the same method on different implementations
197
- result = impl.process_audio_pipeline(audio, "en", voice_settings)
198
- results.append(result)
199
-
200
- assert len(results) == 2
201
- assert results[0].success is True
202
- assert results[1].success is False
203
-
204
- def test_interface_inheritance_chain(self):
205
- """Test the inheritance chain of the interface."""
206
- # Check that it inherits from ABC
207
- assert ABC in IAudioProcessingService.__mro__
208
-
209
- # Check that it's at the right position in MRO
210
- mro = IAudioProcessingService.__mro__
211
- assert mro[0] == IAudioProcessingService
212
- assert ABC in mro
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/interfaces/test_speech_recognition.py DELETED
@@ -1,241 +0,0 @@
1
- """Unit tests for ISpeechRecognitionService interface contract."""
2
-
3
- import pytest
4
- from abc import ABC
5
- from unittest.mock import Mock
6
- from src.domain.interfaces.speech_recognition import ISpeechRecognitionService
7
- from src.domain.models.audio_content import AudioContent
8
- from src.domain.models.text_content import TextContent
9
-
10
-
11
- class TestISpeechRecognitionService:
12
- """Test cases for ISpeechRecognitionService interface contract."""
13
-
14
- def test_interface_is_abstract(self):
15
- """Test that ISpeechRecognitionService is an abstract base class."""
16
- assert issubclass(ISpeechRecognitionService, ABC)
17
-
18
- # Should not be able to instantiate directly
19
- with pytest.raises(TypeError):
20
- ISpeechRecognitionService() # type: ignore
21
-
22
- def test_interface_has_required_method(self):
23
- """Test that interface defines the required abstract method."""
24
- # Check that the method exists and is abstract
25
- assert hasattr(ISpeechRecognitionService, 'transcribe')
26
- assert getattr(ISpeechRecognitionService.transcribe, '__isabstractmethod__', False)
27
-
28
- def test_method_signature(self):
29
- """Test that the method has the correct signature."""
30
- import inspect
31
-
32
- method = ISpeechRecognitionService.transcribe
33
- signature = inspect.signature(method)
34
-
35
- # Check parameter names
36
- params = list(signature.parameters.keys())
37
- expected_params = ['self', 'audio', 'model']
38
-
39
- assert params == expected_params
40
-
41
- # Check return annotation
42
- assert signature.return_annotation == "'TextContent'"
43
-
44
- def test_concrete_implementation_must_implement_method(self):
45
- """Test that concrete implementations must implement the abstract method."""
46
-
47
- class IncompleteImplementation(ISpeechRecognitionService):
48
- pass
49
-
50
- # Should not be able to instantiate without implementing abstract method
51
- with pytest.raises(TypeError, match="Can't instantiate abstract class"):
52
- IncompleteImplementation() # type: ignore
53
-
54
- def test_concrete_implementation_with_method(self):
55
- """Test that concrete implementation with method can be instantiated."""
56
-
57
- class ConcreteImplementation(ISpeechRecognitionService):
58
- def transcribe(self, audio, model):
59
- return TextContent(text="transcribed text", language="en")
60
-
61
- # Should be able to instantiate
62
- implementation = ConcreteImplementation()
63
- assert isinstance(implementation, ISpeechRecognitionService)
64
-
65
- def test_method_contract_with_mock(self):
66
- """Test the method contract using a mock implementation."""
67
-
68
- class MockImplementation(ISpeechRecognitionService):
69
- def __init__(self):
70
- self.mock_method = Mock()
71
-
72
- def transcribe(self, audio, model):
73
- return self.mock_method(audio, model)
74
-
75
- # Create test data
76
- audio = AudioContent(
77
- data=b"test_audio",
78
- format="wav",
79
- sample_rate=22050,
80
- duration=5.0
81
- )
82
- model = "whisper-base"
83
- expected_result = TextContent(text="Hello world", language="en")
84
-
85
- # Setup mock
86
- implementation = MockImplementation()
87
- implementation.mock_method.return_value = expected_result
88
-
89
- # Call method
90
- result = implementation.transcribe(audio=audio, model=model)
91
-
92
- # Verify call and result
93
- implementation.mock_method.assert_called_once_with(audio, model)
94
- assert result == expected_result
95
-
96
- def test_interface_docstring_requirements(self):
97
- """Test that the interface method has proper documentation."""
98
- method = ISpeechRecognitionService.transcribe
99
-
100
- assert method.__doc__ is not None
101
- docstring = method.__doc__
102
-
103
- # Check that docstring contains key information
104
- assert "Transcribe audio content to text" in docstring
105
- assert "Args:" in docstring
106
- assert "Returns:" in docstring
107
- assert "Raises:" in docstring
108
- assert "SpeechRecognitionException" in docstring
109
-
110
- def test_interface_type_hints(self):
111
- """Test that the interface uses proper type hints."""
112
- method = ISpeechRecognitionService.transcribe
113
- annotations = getattr(method, '__annotations__', {})
114
-
115
- assert 'audio' in annotations
116
- assert 'model' in annotations
117
- assert 'return' in annotations
118
-
119
- # Check that type annotations are correct
120
- assert annotations['audio'] == "'AudioContent'"
121
- assert annotations['model'] == str
122
- assert annotations['return'] == "'TextContent'"
123
-
124
- def test_multiple_implementations_possible(self):
125
- """Test that multiple implementations of the interface are possible."""
126
-
127
- class WhisperImplementation(ISpeechRecognitionService):
128
- def transcribe(self, audio, model):
129
- return TextContent(text="whisper transcription", language="en")
130
-
131
- class ParakeetImplementation(ISpeechRecognitionService):
132
- def transcribe(self, audio, model):
133
- return TextContent(text="parakeet transcription", language="en")
134
-
135
- whisper = WhisperImplementation()
136
- parakeet = ParakeetImplementation()
137
-
138
- assert isinstance(whisper, ISpeechRecognitionService)
139
- assert isinstance(parakeet, ISpeechRecognitionService)
140
- assert type(whisper) != type(parakeet)
141
-
142
- def test_interface_method_can_be_called_polymorphically(self):
143
- """Test that interface methods can be called polymorphically."""
144
-
145
- class TestImplementation(ISpeechRecognitionService):
146
- def __init__(self, transcription_text):
147
- self.transcription_text = transcription_text
148
-
149
- def transcribe(self, audio, model):
150
- return TextContent(text=self.transcription_text, language="en")
151
-
152
- # Create different implementations
153
- implementations = [
154
- TestImplementation("first transcription"),
155
- TestImplementation("second transcription")
156
- ]
157
-
158
- # Test polymorphic usage
159
- audio = AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0)
160
- model = "test-model"
161
-
162
- results = []
163
- for impl in implementations:
164
- # Can call the same method on different implementations
165
- result = impl.transcribe(audio, model)
166
- results.append(result.text)
167
-
168
- assert results == ["first transcription", "second transcription"]
169
-
170
- def test_interface_inheritance_chain(self):
171
- """Test the inheritance chain of the interface."""
172
- # Check that it inherits from ABC
173
- assert ABC in ISpeechRecognitionService.__mro__
174
-
175
- # Check that it's at the right position in MRO
176
- mro = ISpeechRecognitionService.__mro__
177
- assert mro[0] == ISpeechRecognitionService
178
- assert ABC in mro
179
-
180
- def test_method_parameter_validation_in_implementation(self):
181
- """Test that implementations can validate parameters."""
182
-
183
- class ValidatingImplementation(ISpeechRecognitionService):
184
- def transcribe(self, audio, model):
185
- if not isinstance(audio, AudioContent):
186
- raise TypeError("audio must be AudioContent")
187
- if not isinstance(model, str):
188
- raise TypeError("model must be string")
189
- if not model.strip():
190
- raise ValueError("model cannot be empty")
191
-
192
- return TextContent(text="validated transcription", language="en")
193
-
194
- impl = ValidatingImplementation()
195
-
196
- # Valid call should work
197
- audio = AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0)
198
- result = impl.transcribe(audio, "whisper-base")
199
- assert result.text == "validated transcription"
200
-
201
- # Invalid calls should raise appropriate errors
202
- with pytest.raises(TypeError, match="audio must be AudioContent"):
203
- impl.transcribe("not audio", "whisper-base") # type: ignore
204
-
205
- with pytest.raises(TypeError, match="model must be string"):
206
- impl.transcribe(audio, 123) # type: ignore
207
-
208
- with pytest.raises(ValueError, match="model cannot be empty"):
209
- impl.transcribe(audio, "")
210
-
211
- def test_implementation_can_handle_different_models(self):
212
- """Test that implementations can handle different model types."""
213
-
214
- class MultiModelImplementation(ISpeechRecognitionService):
215
- def transcribe(self, audio, model):
216
- model_responses = {
217
- "whisper-tiny": "tiny transcription",
218
- "whisper-base": "base transcription",
219
- "whisper-large": "large transcription",
220
- "parakeet": "parakeet transcription"
221
- }
222
-
223
- transcription = model_responses.get(model, "unknown model transcription")
224
- return TextContent(text=transcription, language="en")
225
-
226
- impl = MultiModelImplementation()
227
- audio = AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0)
228
-
229
- # Test different models
230
- models_and_expected = [
231
- ("whisper-tiny", "tiny transcription"),
232
- ("whisper-base", "base transcription"),
233
- ("whisper-large", "large transcription"),
234
- ("parakeet", "parakeet transcription"),
235
- ("unknown-model", "unknown model transcription")
236
- ]
237
-
238
- for model, expected_text in models_and_expected:
239
- result = impl.transcribe(audio, model)
240
- assert result.text == expected_text
241
- assert result.language == "en"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/interfaces/test_speech_synthesis.py DELETED
@@ -1,378 +0,0 @@
1
- """Unit tests for ISpeechSynthesisService interface contract."""
2
-
3
- import pytest
4
- from abc import ABC
5
- from unittest.mock import Mock
6
- from typing import Iterator
7
- from src.domain.interfaces.speech_synthesis import ISpeechSynthesisService
8
- from src.domain.models.speech_synthesis_request import SpeechSynthesisRequest
9
- from src.domain.models.audio_content import AudioContent
10
- from src.domain.models.audio_chunk import AudioChunk
11
- from src.domain.models.text_content import TextContent
12
- from src.domain.models.voice_settings import VoiceSettings
13
-
14
-
15
- class TestISpeechSynthesisService:
16
- """Test cases for ISpeechSynthesisService interface contract."""
17
-
18
- def test_interface_is_abstract(self):
19
- """Test that ISpeechSynthesisService is an abstract base class."""
20
- assert issubclass(ISpeechSynthesisService, ABC)
21
-
22
- # Should not be able to instantiate directly
23
- with pytest.raises(TypeError):
24
- ISpeechSynthesisService() # type: ignore
25
-
26
- def test_interface_has_required_methods(self):
27
- """Test that interface defines the required abstract methods."""
28
- # Check that both methods exist and are abstract
29
- assert hasattr(ISpeechSynthesisService, 'synthesize')
30
- assert hasattr(ISpeechSynthesisService, 'synthesize_stream')
31
-
32
- assert getattr(ISpeechSynthesisService.synthesize, '__isabstractmethod__', False)
33
- assert getattr(ISpeechSynthesisService.synthesize_stream, '__isabstractmethod__', False)
34
-
35
- def test_synthesize_method_signature(self):
36
- """Test that the synthesize method has the correct signature."""
37
- import inspect
38
-
39
- method = ISpeechSynthesisService.synthesize
40
- signature = inspect.signature(method)
41
-
42
- # Check parameter names
43
- params = list(signature.parameters.keys())
44
- expected_params = ['self', 'request']
45
-
46
- assert params == expected_params
47
-
48
- # Check return annotation
49
- assert signature.return_annotation == "'AudioContent'"
50
-
51
- def test_synthesize_stream_method_signature(self):
52
- """Test that the synthesize_stream method has the correct signature."""
53
- import inspect
54
-
55
- method = ISpeechSynthesisService.synthesize_stream
56
- signature = inspect.signature(method)
57
-
58
- # Check parameter names
59
- params = list(signature.parameters.keys())
60
- expected_params = ['self', 'request']
61
-
62
- assert params == expected_params
63
-
64
- # Check return annotation
65
- assert signature.return_annotation == "Iterator['AudioChunk']"
66
-
67
- def test_concrete_implementation_must_implement_methods(self):
68
- """Test that concrete implementations must implement both abstract methods."""
69
-
70
- class IncompleteImplementation(ISpeechSynthesisService):
71
- def synthesize(self, request):
72
- return AudioContent(data=b"test", format="wav", sample_rate=22050, duration=1.0)
73
- # Missing synthesize_stream method
74
-
75
- # Should not be able to instantiate without implementing all abstract methods
76
- with pytest.raises(TypeError, match="Can't instantiate abstract class"):
77
- IncompleteImplementation() # type: ignore
78
-
79
- def test_concrete_implementation_with_both_methods(self):
80
- """Test that concrete implementation with both methods can be instantiated."""
81
-
82
- class ConcreteImplementation(ISpeechSynthesisService):
83
- def synthesize(self, request):
84
- return AudioContent(data=b"synthesized", format="wav", sample_rate=22050, duration=1.0)
85
-
86
- def synthesize_stream(self, request):
87
- yield AudioChunk(data=b"chunk1", format="wav", sample_rate=22050, chunk_index=0, is_final=True)
88
-
89
- # Should be able to instantiate
90
- implementation = ConcreteImplementation()
91
- assert isinstance(implementation, ISpeechSynthesisService)
92
-
93
- def test_synthesize_method_contract_with_mock(self):
94
- """Test the synthesize method contract using a mock implementation."""
95
-
96
- class MockImplementation(ISpeechSynthesisService):
97
- def __init__(self):
98
- self.mock_synthesize = Mock()
99
- self.mock_synthesize_stream = Mock()
100
-
101
- def synthesize(self, request):
102
- return self.mock_synthesize(request)
103
-
104
- def synthesize_stream(self, request):
105
- return self.mock_synthesize_stream(request)
106
-
107
- # Create test data
108
- text_content = TextContent(text="Hello world", language="en")
109
- voice_settings = VoiceSettings(voice_id="test_voice", speed=1.0, language="en")
110
- request = SpeechSynthesisRequest(
111
- text_content=text_content,
112
- voice_settings=voice_settings
113
- )
114
- expected_result = AudioContent(
115
- data=b"synthesized_audio",
116
- format="wav",
117
- sample_rate=22050,
118
- duration=2.0
119
- )
120
-
121
- # Setup mock
122
- implementation = MockImplementation()
123
- implementation.mock_synthesize.return_value = expected_result
124
-
125
- # Call method
126
- result = implementation.synthesize(request)
127
-
128
- # Verify call and result
129
- implementation.mock_synthesize.assert_called_once_with(request)
130
- assert result == expected_result
131
-
132
- def test_synthesize_stream_method_contract_with_mock(self):
133
- """Test the synthesize_stream method contract using a mock implementation."""
134
-
135
- class MockImplementation(ISpeechSynthesisService):
136
- def __init__(self):
137
- self.mock_synthesize = Mock()
138
- self.mock_synthesize_stream = Mock()
139
-
140
- def synthesize(self, request):
141
- return self.mock_synthesize(request)
142
-
143
- def synthesize_stream(self, request):
144
- return self.mock_synthesize_stream(request)
145
-
146
- # Create test data
147
- text_content = TextContent(text="Hello world", language="en")
148
- voice_settings = VoiceSettings(voice_id="test_voice", speed=1.0, language="en")
149
- request = SpeechSynthesisRequest(
150
- text_content=text_content,
151
- voice_settings=voice_settings
152
- )
153
- expected_chunks = [
154
- AudioChunk(data=b"chunk1", format="wav", sample_rate=22050, chunk_index=0),
155
- AudioChunk(data=b"chunk2", format="wav", sample_rate=22050, chunk_index=1, is_final=True)
156
- ]
157
-
158
- # Setup mock
159
- implementation = MockImplementation()
160
- implementation.mock_synthesize_stream.return_value = iter(expected_chunks)
161
-
162
- # Call method
163
- result = implementation.synthesize_stream(request)
164
-
165
- # Verify call and result
166
- implementation.mock_synthesize_stream.assert_called_once_with(request)
167
- chunks = list(result)
168
- assert chunks == expected_chunks
169
-
170
- def test_interface_docstring_requirements(self):
171
- """Test that the interface methods have proper documentation."""
172
- synthesize_method = ISpeechSynthesisService.synthesize
173
- stream_method = ISpeechSynthesisService.synthesize_stream
174
-
175
- # Check synthesize method docstring
176
- assert synthesize_method.__doc__ is not None
177
- synthesize_doc = synthesize_method.__doc__
178
- assert "Synthesize speech from text" in synthesize_doc
179
- assert "Args:" in synthesize_doc
180
- assert "Returns:" in synthesize_doc
181
- assert "Raises:" in synthesize_doc
182
- assert "SpeechSynthesisException" in synthesize_doc
183
-
184
- # Check synthesize_stream method docstring
185
- assert stream_method.__doc__ is not None
186
- stream_doc = stream_method.__doc__
187
- assert "Synthesize speech from text as a stream" in stream_doc
188
- assert "Args:" in stream_doc
189
- assert "Returns:" in stream_doc
190
- assert "Iterator[AudioChunk]" in stream_doc
191
- assert "Raises:" in stream_doc
192
- assert "SpeechSynthesisException" in stream_doc
193
-
194
- def test_interface_type_hints(self):
195
- """Test that the interface uses proper type hints."""
196
- synthesize_method = ISpeechSynthesisService.synthesize
197
- stream_method = ISpeechSynthesisService.synthesize_stream
198
-
199
- # Check synthesize method annotations
200
- synthesize_annotations = getattr(synthesize_method, '__annotations__', {})
201
- assert 'request' in synthesize_annotations
202
- assert 'return' in synthesize_annotations
203
- assert synthesize_annotations['request'] == "'SpeechSynthesisRequest'"
204
- assert synthesize_annotations['return'] == "'AudioContent'"
205
-
206
- # Check synthesize_stream method annotations
207
- stream_annotations = getattr(stream_method, '__annotations__', {})
208
- assert 'request' in stream_annotations
209
- assert 'return' in stream_annotations
210
- assert stream_annotations['request'] == "'SpeechSynthesisRequest'"
211
- assert stream_annotations['return'] == "Iterator['AudioChunk']"
212
-
213
- def test_multiple_implementations_possible(self):
214
- """Test that multiple implementations of the interface are possible."""
215
-
216
- class KokoroImplementation(ISpeechSynthesisService):
217
- def synthesize(self, request):
218
- return AudioContent(data=b"chatterbox_audio", format="wav", sample_rate=22050, duration=1.0)
219
-
220
- def synthesize_stream(self, request):
221
- yield AudioChunk(data=b"chatterbox_chunk", format="wav", sample_rate=22050, chunk_index=0, is_final=True)
222
-
223
- class DiaImplementation(ISpeechSynthesisService):
224
- def synthesize(self, request):
225
- return AudioContent(data=b"chatterbox2_audio", format="wav", sample_rate=22050, duration=1.0)
226
-
227
- def synthesize_stream(self, request):
228
- yield AudioChunk(data=b"chatterbox2_chunk", format="wav", sample_rate=22050, chunk_index=0, is_final=True)
229
-
230
- chatterbox1 = KokoroImplementation()
231
- chatterbox2 = DiaImplementation()
232
-
233
- assert isinstance(chatterbox1, ISpeechSynthesisService)
234
- assert isinstance(chatterbox2, ISpeechSynthesisService)
235
- assert type(chatterbox1) != type(chatterbox2)
236
-
237
- def test_interface_methods_can_be_called_polymorphically(self):
238
- """Test that interface methods can be called polymorphically."""
239
-
240
- class TestImplementation(ISpeechSynthesisService):
241
- def __init__(self, audio_data, chunk_data):
242
- self.audio_data = audio_data
243
- self.chunk_data = chunk_data
244
-
245
- def synthesize(self, request):
246
- return AudioContent(data=self.audio_data, format="wav", sample_rate=22050, duration=1.0)
247
-
248
- def synthesize_stream(self, request):
249
- yield AudioChunk(data=self.chunk_data, format="wav", sample_rate=22050, chunk_index=0, is_final=True)
250
-
251
- # Create different implementations
252
- implementations = [
253
- TestImplementation(b"audio1", b"chunk1"),
254
- TestImplementation(b"audio2", b"chunk2")
255
- ]
256
-
257
- # Test polymorphic usage
258
- text_content = TextContent(text="test", language="en")
259
- voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en")
260
- request = SpeechSynthesisRequest(text_content=text_content, voice_settings=voice_settings)
261
-
262
- # Test synthesize method
263
- audio_results = []
264
- for impl in implementations:
265
- result = impl.synthesize(request)
266
- audio_results.append(result.data)
267
-
268
- assert audio_results == [b"audio1", b"audio2"]
269
-
270
- # Test synthesize_stream method
271
- chunk_results = []
272
- for impl in implementations:
273
- chunks = list(impl.synthesize_stream(request))
274
- chunk_results.append(chunks[0].data)
275
-
276
- assert chunk_results == [b"chunk1", b"chunk2"]
277
-
278
- def test_interface_inheritance_chain(self):
279
- """Test the inheritance chain of the interface."""
280
- # Check that it inherits from ABC
281
- assert ABC in ISpeechSynthesisService.__mro__
282
-
283
- # Check that it's at the right position in MRO
284
- mro = ISpeechSynthesisService.__mro__
285
- assert mro[0] == ISpeechSynthesisService
286
- assert ABC in mro
287
-
288
- def test_stream_method_returns_iterator(self):
289
- """Test that synthesize_stream returns an iterator."""
290
-
291
- class StreamingImplementation(ISpeechSynthesisService):
292
- def synthesize(self, request):
293
- return AudioContent(data=b"audio", format="wav", sample_rate=22050, duration=1.0)
294
-
295
- def synthesize_stream(self, request):
296
- for i in range(3):
297
- yield AudioChunk(
298
- data=f"chunk{i}".encode(),
299
- format="wav",
300
- sample_rate=22050,
301
- chunk_index=i,
302
- is_final=(i == 2)
303
- )
304
-
305
- impl = StreamingImplementation()
306
- text_content = TextContent(text="test", language="en")
307
- voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en")
308
- request = SpeechSynthesisRequest(text_content=text_content, voice_settings=voice_settings)
309
-
310
- # Get the iterator
311
- stream = impl.synthesize_stream(request)
312
-
313
- # Verify it's an iterator
314
- assert hasattr(stream, '__iter__')
315
- assert hasattr(stream, '__next__')
316
-
317
- # Verify we can iterate through chunks
318
- chunks = list(stream)
319
- assert len(chunks) == 3
320
-
321
- for i, chunk in enumerate(chunks):
322
- assert chunk.data == f"chunk{i}".encode()
323
- assert chunk.chunk_index == i
324
- assert chunk.is_final == (i == 2)
325
-
326
- def test_implementation_can_handle_different_formats(self):
327
- """Test that implementations can handle different output formats."""
328
-
329
- class MultiFormatImplementation(ISpeechSynthesisService):
330
- def synthesize(self, request):
331
- format_data = {
332
- "wav": b"wav_audio_data",
333
- "mp3": b"mp3_audio_data",
334
- "flac": b"flac_audio_data",
335
- "ogg": b"ogg_audio_data"
336
- }
337
-
338
- audio_data = format_data.get(request.output_format, b"default_audio_data")
339
- return AudioContent(
340
- data=audio_data,
341
- format=request.output_format,
342
- sample_rate=request.effective_sample_rate,
343
- duration=1.0
344
- )
345
-
346
- def synthesize_stream(self, request):
347
- yield AudioChunk(
348
- data=f"{request.output_format}_chunk".encode(),
349
- format=request.output_format,
350
- sample_rate=request.effective_sample_rate,
351
- chunk_index=0,
352
- is_final=True
353
- )
354
-
355
- impl = MultiFormatImplementation()
356
- text_content = TextContent(text="test", language="en")
357
- voice_settings = VoiceSettings(voice_id="test", speed=1.0, language="en")
358
-
359
- # Test different formats
360
- formats = ["wav", "mp3", "flac", "ogg"]
361
-
362
- for fmt in formats:
363
- request = SpeechSynthesisRequest(
364
- text_content=text_content,
365
- voice_settings=voice_settings,
366
- output_format=fmt
367
- )
368
-
369
- # Test synthesize
370
- audio = impl.synthesize(request)
371
- assert audio.format == fmt
372
- assert audio.data == f"{fmt}_audio_data".encode()
373
-
374
- # Test synthesize_stream
375
- chunks = list(impl.synthesize_stream(request))
376
- assert len(chunks) == 1
377
- assert chunks[0].format == fmt
378
- assert chunks[0].data == f"{fmt}_chunk".encode()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/interfaces/test_translation.py DELETED
@@ -1,303 +0,0 @@
1
- """Unit tests for ITranslationService interface contract."""
2
-
3
- import pytest
4
- from abc import ABC
5
- from unittest.mock import Mock
6
- from src.domain.interfaces.translation import ITranslationService
7
- from src.domain.models.translation_request import TranslationRequest
8
- from src.domain.models.text_content import TextContent
9
-
10
-
11
- class TestITranslationService:
12
- """Test cases for ITranslationService interface contract."""
13
-
14
- def test_interface_is_abstract(self):
15
- """Test that ITranslationService is an abstract base class."""
16
- assert issubclass(ITranslationService, ABC)
17
-
18
- # Should not be able to instantiate directly
19
- with pytest.raises(TypeError):
20
- ITranslationService() # type: ignore
21
-
22
- def test_interface_has_required_method(self):
23
- """Test that interface defines the required abstract method."""
24
- # Check that the method exists and is abstract
25
- assert hasattr(ITranslationService, 'translate')
26
- assert getattr(ITranslationService.translate, '__isabstractmethod__', False)
27
-
28
- def test_method_signature(self):
29
- """Test that the method has the correct signature."""
30
- import inspect
31
-
32
- method = ITranslationService.translate
33
- signature = inspect.signature(method)
34
-
35
- # Check parameter names
36
- params = list(signature.parameters.keys())
37
- expected_params = ['self', 'request']
38
-
39
- assert params == expected_params
40
-
41
- # Check return annotation
42
- assert signature.return_annotation == "'TextContent'"
43
-
44
- def test_concrete_implementation_must_implement_method(self):
45
- """Test that concrete implementations must implement the abstract method."""
46
-
47
- class IncompleteImplementation(ITranslationService):
48
- pass
49
-
50
- # Should not be able to instantiate without implementing abstract method
51
- with pytest.raises(TypeError, match="Can't instantiate abstract class"):
52
- IncompleteImplementation() # type: ignore
53
-
54
- def test_concrete_implementation_with_method(self):
55
- """Test that concrete implementation with method can be instantiated."""
56
-
57
- class ConcreteImplementation(ITranslationService):
58
- def translate(self, request):
59
- return TextContent(text="translated text", language=request.target_language)
60
-
61
- # Should be able to instantiate
62
- implementation = ConcreteImplementation()
63
- assert isinstance(implementation, ITranslationService)
64
-
65
- def test_method_contract_with_mock(self):
66
- """Test the method contract using a mock implementation."""
67
-
68
- class MockImplementation(ITranslationService):
69
- def __init__(self):
70
- self.mock_method = Mock()
71
-
72
- def translate(self, request):
73
- return self.mock_method(request)
74
-
75
- # Create test data
76
- source_text = TextContent(text="Hello world", language="en")
77
- request = TranslationRequest(
78
- source_text=source_text,
79
- target_language="es"
80
- )
81
- expected_result = TextContent(text="Hola mundo", language="es")
82
-
83
- # Setup mock
84
- implementation = MockImplementation()
85
- implementation.mock_method.return_value = expected_result
86
-
87
- # Call method
88
- result = implementation.translate(request)
89
-
90
- # Verify call and result
91
- implementation.mock_method.assert_called_once_with(request)
92
- assert result == expected_result
93
-
94
- def test_interface_docstring_requirements(self):
95
- """Test that the interface method has proper documentation."""
96
- method = ITranslationService.translate
97
-
98
- assert method.__doc__ is not None
99
- docstring = method.__doc__
100
-
101
- # Check that docstring contains key information
102
- assert "Translate text from source language to target language" in docstring
103
- assert "Args:" in docstring
104
- assert "Returns:" in docstring
105
- assert "Raises:" in docstring
106
- assert "TranslationFailedException" in docstring
107
-
108
- def test_interface_type_hints(self):
109
- """Test that the interface uses proper type hints."""
110
- method = ITranslationService.translate
111
- annotations = getattr(method, '__annotations__', {})
112
-
113
- assert 'request' in annotations
114
- assert 'return' in annotations
115
-
116
- # Check that type annotations are correct
117
- assert annotations['request'] == "'TranslationRequest'"
118
- assert annotations['return'] == "'TextContent'"
119
-
120
- def test_multiple_implementations_possible(self):
121
- """Test that multiple implementations of the interface are possible."""
122
-
123
- class NLLBImplementation(ITranslationService):
124
- def translate(self, request):
125
- return TextContent(text="NLLB translation", language=request.target_language)
126
-
127
- class GoogleImplementation(ITranslationService):
128
- def translate(self, request):
129
- return TextContent(text="Google translation", language=request.target_language)
130
-
131
- nllb = NLLBImplementation()
132
- google = GoogleImplementation()
133
-
134
- assert isinstance(nllb, ITranslationService)
135
- assert isinstance(google, ITranslationService)
136
- assert type(nllb) != type(google)
137
-
138
- def test_interface_method_can_be_called_polymorphically(self):
139
- """Test that interface methods can be called polymorphically."""
140
-
141
- class TestImplementation(ITranslationService):
142
- def __init__(self, translation_prefix):
143
- self.translation_prefix = translation_prefix
144
-
145
- def translate(self, request):
146
- translated_text = f"{self.translation_prefix}: {request.source_text.text}"
147
- return TextContent(text=translated_text, language=request.target_language)
148
-
149
- # Create different implementations
150
- implementations = [
151
- TestImplementation("Provider1"),
152
- TestImplementation("Provider2")
153
- ]
154
-
155
- # Test polymorphic usage
156
- source_text = TextContent(text="Hello", language="en")
157
- request = TranslationRequest(source_text=source_text, target_language="es")
158
-
159
- results = []
160
- for impl in implementations:
161
- # Can call the same method on different implementations
162
- result = impl.translate(request)
163
- results.append(result.text)
164
-
165
- assert results == ["Provider1: Hello", "Provider2: Hello"]
166
-
167
- def test_interface_inheritance_chain(self):
168
- """Test the inheritance chain of the interface."""
169
- # Check that it inherits from ABC
170
- assert ABC in ITranslationService.__mro__
171
-
172
- # Check that it's at the right position in MRO
173
- mro = ITranslationService.__mro__
174
- assert mro[0] == ITranslationService
175
- assert ABC in mro
176
-
177
- def test_method_parameter_validation_in_implementation(self):
178
- """Test that implementations can validate parameters."""
179
-
180
- class ValidatingImplementation(ITranslationService):
181
- def translate(self, request):
182
- if not isinstance(request, TranslationRequest):
183
- raise TypeError("request must be TranslationRequest")
184
-
185
- # Validate that source and target languages are different
186
- if request.effective_source_language == request.target_language:
187
- raise ValueError("Source and target languages cannot be the same")
188
-
189
- return TextContent(
190
- text=f"Translated: {request.source_text.text}",
191
- language=request.target_language
192
- )
193
-
194
- impl = ValidatingImplementation()
195
-
196
- # Valid call should work
197
- source_text = TextContent(text="Hello", language="en")
198
- request = TranslationRequest(source_text=source_text, target_language="es")
199
- result = impl.translate(request)
200
- assert result.text == "Translated: Hello"
201
- assert result.language == "es"
202
-
203
- # Invalid parameter type should raise error
204
- with pytest.raises(TypeError, match="request must be TranslationRequest"):
205
- impl.translate("not a request") # type: ignore
206
-
207
- # Same language should raise error
208
- same_lang_text = TextContent(text="Hello", language="en")
209
- same_lang_request = TranslationRequest(source_text=same_lang_text, target_language="en")
210
- with pytest.raises(ValueError, match="Source and target languages cannot be the same"):
211
- impl.translate(same_lang_request)
212
-
213
- def test_implementation_can_handle_different_language_pairs(self):
214
- """Test that implementations can handle different language pairs."""
215
-
216
- class MultiLanguageImplementation(ITranslationService):
217
- def __init__(self):
218
- self.translations = {
219
- ("en", "es"): {"Hello": "Hola", "world": "mundo"},
220
- ("en", "fr"): {"Hello": "Bonjour", "world": "monde"},
221
- ("es", "en"): {"Hola": "Hello", "mundo": "world"},
222
- ("fr", "en"): {"Bonjour": "Hello", "monde": "world"}
223
- }
224
-
225
- def translate(self, request):
226
- source_lang = request.effective_source_language
227
- target_lang = request.target_language
228
-
229
- translation_dict = self.translations.get((source_lang, target_lang), {})
230
-
231
- # Simple word-by-word translation for testing
232
- words = request.source_text.text.split()
233
- translated_words = [translation_dict.get(word, word) for word in words]
234
- translated_text = " ".join(translated_words)
235
-
236
- return TextContent(text=translated_text, language=target_lang)
237
-
238
- impl = MultiLanguageImplementation()
239
-
240
- # Test different language pairs
241
- test_cases = [
242
- ("Hello world", "en", "es", "Hola mundo"),
243
- ("Hello world", "en", "fr", "Bonjour monde"),
244
- ("Hola mundo", "es", "en", "Hello world"),
245
- ("Bonjour monde", "fr", "en", "Hello world")
246
- ]
247
-
248
- for text, source_lang, target_lang, expected in test_cases:
249
- source_text = TextContent(text=text, language=source_lang)
250
- request = TranslationRequest(
251
- source_text=source_text,
252
- target_language=target_lang,
253
- source_language=source_lang
254
- )
255
-
256
- result = impl.translate(request)
257
- assert result.text == expected
258
- assert result.language == target_lang
259
-
260
- def test_implementation_can_handle_auto_detect_source_language(self):
261
- """Test that implementations can handle auto-detection of source language."""
262
-
263
- class AutoDetectImplementation(ITranslationService):
264
- def translate(self, request):
265
- # Use the effective source language (from TextContent if not explicitly set)
266
- source_lang = request.effective_source_language
267
- target_lang = request.target_language
268
-
269
- # Simple mock translation based on detected language
270
- if source_lang == "en" and target_lang == "es":
271
- translated_text = f"ES: {request.source_text.text}"
272
- elif source_lang == "es" and target_lang == "en":
273
- translated_text = f"EN: {request.source_text.text}"
274
- else:
275
- translated_text = f"UNKNOWN: {request.source_text.text}"
276
-
277
- return TextContent(text=translated_text, language=target_lang)
278
-
279
- impl = AutoDetectImplementation()
280
-
281
- # Test with explicit source language
282
- source_text = TextContent(text="Hello", language="en")
283
- explicit_request = TranslationRequest(
284
- source_text=source_text,
285
- target_language="es",
286
- source_language="en"
287
- )
288
-
289
- result = impl.translate(explicit_request)
290
- assert result.text == "ES: Hello"
291
- assert result.language == "es"
292
-
293
- # Test with auto-detected source language (None)
294
- auto_request = TranslationRequest(
295
- source_text=source_text, # language="en" in TextContent
296
- target_language="es"
297
- # source_language=None (default)
298
- )
299
-
300
- result = impl.translate(auto_request)
301
- assert result.text == "ES: Hello" # Should use language from TextContent
302
- assert result.language == "es"
303
- assert auto_request.is_auto_detect_source is True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/models/test_audio_chunk.py DELETED
@@ -1,322 +0,0 @@
1
- """Unit tests for AudioChunk value object."""
2
-
3
- import pytest
4
- from src.domain.models.audio_chunk import AudioChunk
5
-
6
-
7
- class TestAudioChunk:
8
- """Test cases for AudioChunk value object."""
9
-
10
- def test_valid_audio_chunk_creation(self):
11
- """Test creating valid AudioChunk instance."""
12
- chunk = AudioChunk(
13
- data=b"fake_audio_chunk_data",
14
- format="wav",
15
- sample_rate=22050,
16
- chunk_index=0,
17
- is_final=False,
18
- timestamp=1.5
19
- )
20
-
21
- assert chunk.data == b"fake_audio_chunk_data"
22
- assert chunk.format == "wav"
23
- assert chunk.sample_rate == 22050
24
- assert chunk.chunk_index == 0
25
- assert chunk.is_final is False
26
- assert chunk.timestamp == 1.5
27
- assert chunk.size_bytes == len(b"fake_audio_chunk_data")
28
-
29
- def test_audio_chunk_with_defaults(self):
30
- """Test creating AudioChunk with default values."""
31
- chunk = AudioChunk(
32
- data=b"fake_audio_chunk_data",
33
- format="wav",
34
- sample_rate=22050,
35
- chunk_index=0
36
- )
37
-
38
- assert chunk.is_final is False
39
- assert chunk.timestamp is None
40
-
41
- def test_final_chunk_creation(self):
42
- """Test creating final AudioChunk."""
43
- chunk = AudioChunk(
44
- data=b"final_chunk_data",
45
- format="wav",
46
- sample_rate=22050,
47
- chunk_index=5,
48
- is_final=True
49
- )
50
-
51
- assert chunk.is_final is True
52
- assert chunk.chunk_index == 5
53
-
54
- def test_non_bytes_data_raises_error(self):
55
- """Test that non-bytes data raises TypeError."""
56
- with pytest.raises(TypeError, match="Audio data must be bytes"):
57
- AudioChunk(
58
- data="not_bytes", # type: ignore
59
- format="wav",
60
- sample_rate=22050,
61
- chunk_index=0
62
- )
63
-
64
- def test_empty_data_raises_error(self):
65
- """Test that empty data raises ValueError."""
66
- with pytest.raises(ValueError, match="Audio data cannot be empty"):
67
- AudioChunk(
68
- data=b"",
69
- format="wav",
70
- sample_rate=22050,
71
- chunk_index=0
72
- )
73
-
74
- def test_unsupported_format_raises_error(self):
75
- """Test that unsupported format raises ValueError."""
76
- with pytest.raises(ValueError, match="Unsupported audio format: xyz"):
77
- AudioChunk(
78
- data=b"fake_data",
79
- format="xyz",
80
- sample_rate=22050,
81
- chunk_index=0
82
- )
83
-
84
- def test_supported_formats(self):
85
- """Test all supported audio formats."""
86
- supported_formats = ['wav', 'mp3', 'flac', 'ogg', 'raw']
87
-
88
- for fmt in supported_formats:
89
- chunk = AudioChunk(
90
- data=b"fake_data",
91
- format=fmt,
92
- sample_rate=22050,
93
- chunk_index=0
94
- )
95
- assert chunk.format == fmt
96
-
97
- def test_non_integer_sample_rate_raises_error(self):
98
- """Test that non-integer sample rate raises ValueError."""
99
- with pytest.raises(ValueError, match="Sample rate must be a positive integer"):
100
- AudioChunk(
101
- data=b"fake_data",
102
- format="wav",
103
- sample_rate=22050.5, # type: ignore
104
- chunk_index=0
105
- )
106
-
107
- def test_negative_sample_rate_raises_error(self):
108
- """Test that negative sample rate raises ValueError."""
109
- with pytest.raises(ValueError, match="Sample rate must be a positive integer"):
110
- AudioChunk(
111
- data=b"fake_data",
112
- format="wav",
113
- sample_rate=-1,
114
- chunk_index=0
115
- )
116
-
117
- def test_zero_sample_rate_raises_error(self):
118
- """Test that zero sample rate raises ValueError."""
119
- with pytest.raises(ValueError, match="Sample rate must be a positive integer"):
120
- AudioChunk(
121
- data=b"fake_data",
122
- format="wav",
123
- sample_rate=0,
124
- chunk_index=0
125
- )
126
-
127
- def test_non_integer_chunk_index_raises_error(self):
128
- """Test that non-integer chunk index raises ValueError."""
129
- with pytest.raises(ValueError, match="Chunk index must be a non-negative integer"):
130
- AudioChunk(
131
- data=b"fake_data",
132
- format="wav",
133
- sample_rate=22050,
134
- chunk_index=1.5 # type: ignore
135
- )
136
-
137
- def test_negative_chunk_index_raises_error(self):
138
- """Test that negative chunk index raises ValueError."""
139
- with pytest.raises(ValueError, match="Chunk index must be a non-negative integer"):
140
- AudioChunk(
141
- data=b"fake_data",
142
- format="wav",
143
- sample_rate=22050,
144
- chunk_index=-1
145
- )
146
-
147
- def test_valid_chunk_index_zero(self):
148
- """Test that chunk index of zero is valid."""
149
- chunk = AudioChunk(
150
- data=b"fake_data",
151
- format="wav",
152
- sample_rate=22050,
153
- chunk_index=0
154
- )
155
- assert chunk.chunk_index == 0
156
-
157
- def test_non_boolean_is_final_raises_error(self):
158
- """Test that non-boolean is_final raises TypeError."""
159
- with pytest.raises(TypeError, match="is_final must be a boolean"):
160
- AudioChunk(
161
- data=b"fake_data",
162
- format="wav",
163
- sample_rate=22050,
164
- chunk_index=0,
165
- is_final="true" # type: ignore
166
- )
167
-
168
- def test_non_numeric_timestamp_raises_error(self):
169
- """Test that non-numeric timestamp raises ValueError."""
170
- with pytest.raises(ValueError, match="Timestamp must be a non-negative number"):
171
- AudioChunk(
172
- data=b"fake_data",
173
- format="wav",
174
- sample_rate=22050,
175
- chunk_index=0,
176
- timestamp="1.5" # type: ignore
177
- )
178
-
179
- def test_negative_timestamp_raises_error(self):
180
- """Test that negative timestamp raises ValueError."""
181
- with pytest.raises(ValueError, match="Timestamp must be a non-negative number"):
182
- AudioChunk(
183
- data=b"fake_data",
184
- format="wav",
185
- sample_rate=22050,
186
- chunk_index=0,
187
- timestamp=-1.0
188
- )
189
-
190
- def test_valid_timestamp_zero(self):
191
- """Test that timestamp of zero is valid."""
192
- chunk = AudioChunk(
193
- data=b"fake_data",
194
- format="wav",
195
- sample_rate=22050,
196
- chunk_index=0,
197
- timestamp=0.0
198
- )
199
- assert chunk.timestamp == 0.0
200
-
201
- def test_valid_timestamp_values(self):
202
- """Test valid timestamp values."""
203
- valid_timestamps = [0.0, 1.5, 10, 100.123]
204
-
205
- for timestamp in valid_timestamps:
206
- chunk = AudioChunk(
207
- data=b"fake_data",
208
- format="wav",
209
- sample_rate=22050,
210
- chunk_index=0,
211
- timestamp=timestamp
212
- )
213
- assert chunk.timestamp == timestamp
214
-
215
- def test_size_bytes_property(self):
216
- """Test size_bytes property returns correct value."""
217
- test_data = b"test_audio_chunk_data_123"
218
- chunk = AudioChunk(
219
- data=test_data,
220
- format="wav",
221
- sample_rate=22050,
222
- chunk_index=0
223
- )
224
-
225
- assert chunk.size_bytes == len(test_data)
226
-
227
- def test_duration_estimate_property(self):
228
- """Test duration_estimate property calculation."""
229
- # Create chunk with known data size
230
- test_data = b"x" * 44100 # 44100 bytes
231
- chunk = AudioChunk(
232
- data=test_data,
233
- format="wav",
234
- sample_rate=22050, # 22050 samples per second
235
- chunk_index=0
236
- )
237
-
238
- # Expected duration: 44100 bytes / (22050 samples/sec * 2 bytes/sample) = 1.0 second
239
- expected_duration = 44100 / (22050 * 2)
240
- assert abs(chunk.duration_estimate - expected_duration) < 0.01
241
-
242
- def test_duration_estimate_with_zero_sample_rate(self):
243
- """Test duration_estimate with edge case of zero calculation."""
244
- # This shouldn't happen due to validation, but test the property logic
245
- chunk = AudioChunk(
246
- data=b"test_data",
247
- format="wav",
248
- sample_rate=22050,
249
- chunk_index=0
250
- )
251
-
252
- # Should return a reasonable estimate
253
- assert chunk.duration_estimate >= 0
254
-
255
- def test_audio_chunk_is_immutable(self):
256
- """Test that AudioChunk is immutable (frozen dataclass)."""
257
- chunk = AudioChunk(
258
- data=b"fake_data",
259
- format="wav",
260
- sample_rate=22050,
261
- chunk_index=0
262
- )
263
-
264
- with pytest.raises(AttributeError):
265
- chunk.format = "mp3" # type: ignore
266
-
267
- def test_chunk_sequence_ordering(self):
268
- """Test that chunks can be ordered by chunk_index."""
269
- chunks = [
270
- AudioChunk(data=b"chunk2", format="wav", sample_rate=22050, chunk_index=2),
271
- AudioChunk(data=b"chunk0", format="wav", sample_rate=22050, chunk_index=0),
272
- AudioChunk(data=b"chunk1", format="wav", sample_rate=22050, chunk_index=1),
273
- ]
274
-
275
- # Sort by chunk_index
276
- sorted_chunks = sorted(chunks, key=lambda c: c.chunk_index)
277
-
278
- assert sorted_chunks[0].chunk_index == 0
279
- assert sorted_chunks[1].chunk_index == 1
280
- assert sorted_chunks[2].chunk_index == 2
281
-
282
- def test_streaming_scenario(self):
283
- """Test typical streaming scenario with multiple chunks."""
284
- # First chunk
285
- chunk1 = AudioChunk(
286
- data=b"first_chunk_data",
287
- format="wav",
288
- sample_rate=22050,
289
- chunk_index=0,
290
- is_final=False,
291
- timestamp=0.0
292
- )
293
-
294
- # Middle chunk
295
- chunk2 = AudioChunk(
296
- data=b"middle_chunk_data",
297
- format="wav",
298
- sample_rate=22050,
299
- chunk_index=1,
300
- is_final=False,
301
- timestamp=1.0
302
- )
303
-
304
- # Final chunk
305
- chunk3 = AudioChunk(
306
- data=b"final_chunk_data",
307
- format="wav",
308
- sample_rate=22050,
309
- chunk_index=2,
310
- is_final=True,
311
- timestamp=2.0
312
- )
313
-
314
- assert not chunk1.is_final
315
- assert not chunk2.is_final
316
- assert chunk3.is_final
317
-
318
- # Verify ordering
319
- chunks = [chunk1, chunk2, chunk3]
320
- for i, chunk in enumerate(chunks):
321
- assert chunk.chunk_index == i
322
- assert chunk.timestamp == float(i)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/models/test_audio_content.py DELETED
@@ -1,229 +0,0 @@
1
- """Unit tests for AudioContent value object."""
2
-
3
- import pytest
4
- from src.domain.models.audio_content import AudioContent
5
-
6
-
7
- class TestAudioContent:
8
- """Test cases for AudioContent value object."""
9
-
10
- def test_valid_audio_content_creation(self):
11
- """Test creating valid AudioContent instance."""
12
- audio_data = b"fake_audio_data"
13
- audio = AudioContent(
14
- data=audio_data,
15
- format="wav",
16
- sample_rate=44100,
17
- duration=10.5,
18
- filename="test.wav"
19
- )
20
-
21
- assert audio.data == audio_data
22
- assert audio.format == "wav"
23
- assert audio.sample_rate == 44100
24
- assert audio.duration == 10.5
25
- assert audio.filename == "test.wav"
26
- assert audio.size_bytes == len(audio_data)
27
- assert audio.is_valid_format is True
28
-
29
- def test_audio_content_without_filename(self):
30
- """Test creating AudioContent without filename."""
31
- audio = AudioContent(
32
- data=b"fake_audio_data",
33
- format="mp3",
34
- sample_rate=22050,
35
- duration=5.0
36
- )
37
-
38
- assert audio.filename is None
39
- assert audio.format == "mp3"
40
-
41
- def test_empty_audio_data_raises_error(self):
42
- """Test that empty audio data raises ValueError."""
43
- with pytest.raises(ValueError, match="Audio data cannot be empty"):
44
- AudioContent(
45
- data=b"",
46
- format="wav",
47
- sample_rate=44100,
48
- duration=10.0
49
- )
50
-
51
- def test_non_bytes_audio_data_raises_error(self):
52
- """Test that non-bytes audio data raises TypeError."""
53
- with pytest.raises(TypeError, match="Audio data must be bytes"):
54
- AudioContent(
55
- data="not_bytes", # type: ignore
56
- format="wav",
57
- sample_rate=44100,
58
- duration=10.0
59
- )
60
-
61
- def test_unsupported_format_raises_error(self):
62
- """Test that unsupported format raises ValueError."""
63
- with pytest.raises(ValueError, match="Unsupported audio format: xyz"):
64
- AudioContent(
65
- data=b"fake_audio_data",
66
- format="xyz",
67
- sample_rate=44100,
68
- duration=10.0
69
- )
70
-
71
- def test_supported_formats(self):
72
- """Test all supported audio formats."""
73
- supported_formats = ['wav', 'mp3', 'flac', 'ogg']
74
-
75
- for fmt in supported_formats:
76
- audio = AudioContent(
77
- data=b"fake_audio_data",
78
- format=fmt,
79
- sample_rate=44100,
80
- duration=10.0
81
- )
82
- assert audio.format == fmt
83
- assert audio.is_valid_format is True
84
-
85
- def test_negative_sample_rate_raises_error(self):
86
- """Test that negative sample rate raises ValueError."""
87
- with pytest.raises(ValueError, match="Sample rate must be positive"):
88
- AudioContent(
89
- data=b"fake_audio_data",
90
- format="wav",
91
- sample_rate=-1,
92
- duration=10.0
93
- )
94
-
95
- def test_zero_sample_rate_raises_error(self):
96
- """Test that zero sample rate raises ValueError."""
97
- with pytest.raises(ValueError, match="Sample rate must be positive"):
98
- AudioContent(
99
- data=b"fake_audio_data",
100
- format="wav",
101
- sample_rate=0,
102
- duration=10.0
103
- )
104
-
105
- def test_sample_rate_too_low_raises_error(self):
106
- """Test that sample rate below 8000 raises ValueError."""
107
- with pytest.raises(ValueError, match="Sample rate must be between 8000 and 192000 Hz"):
108
- AudioContent(
109
- data=b"fake_audio_data",
110
- format="wav",
111
- sample_rate=7999,
112
- duration=10.0
113
- )
114
-
115
- def test_sample_rate_too_high_raises_error(self):
116
- """Test that sample rate above 192000 raises ValueError."""
117
- with pytest.raises(ValueError, match="Sample rate must be between 8000 and 192000 Hz"):
118
- AudioContent(
119
- data=b"fake_audio_data",
120
- format="wav",
121
- sample_rate=192001,
122
- duration=10.0
123
- )
124
-
125
- def test_valid_sample_rate_boundaries(self):
126
- """Test valid sample rate boundaries."""
127
- # Test minimum valid sample rate
128
- audio_min = AudioContent(
129
- data=b"fake_audio_data",
130
- format="wav",
131
- sample_rate=8000,
132
- duration=10.0
133
- )
134
- assert audio_min.sample_rate == 8000
135
-
136
- # Test maximum valid sample rate
137
- audio_max = AudioContent(
138
- data=b"fake_audio_data",
139
- format="wav",
140
- sample_rate=192000,
141
- duration=10.0
142
- )
143
- assert audio_max.sample_rate == 192000
144
-
145
- def test_negative_duration_raises_error(self):
146
- """Test that negative duration raises ValueError."""
147
- with pytest.raises(ValueError, match="Duration must be positive"):
148
- AudioContent(
149
- data=b"fake_audio_data",
150
- format="wav",
151
- sample_rate=44100,
152
- duration=-1.0
153
- )
154
-
155
- def test_zero_duration_raises_error(self):
156
- """Test that zero duration raises ValueError."""
157
- with pytest.raises(ValueError, match="Duration must be positive"):
158
- AudioContent(
159
- data=b"fake_audio_data",
160
- format="wav",
161
- sample_rate=44100,
162
- duration=0.0
163
- )
164
-
165
- def test_duration_too_long_raises_error(self):
166
- """Test that duration over 1 hour raises ValueError."""
167
- with pytest.raises(ValueError, match="Audio duration cannot exceed 1 hour"):
168
- AudioContent(
169
- data=b"fake_audio_data",
170
- format="wav",
171
- sample_rate=44100,
172
- duration=3601.0 # 1 hour + 1 second
173
- )
174
-
175
- def test_valid_duration_boundary(self):
176
- """Test valid duration boundary (exactly 1 hour)."""
177
- audio = AudioContent(
178
- data=b"fake_audio_data",
179
- format="wav",
180
- sample_rate=44100,
181
- duration=3600.0 # Exactly 1 hour
182
- )
183
- assert audio.duration == 3600.0
184
-
185
- def test_empty_filename_raises_error(self):
186
- """Test that empty filename raises ValueError."""
187
- with pytest.raises(ValueError, match="Filename cannot be empty string"):
188
- AudioContent(
189
- data=b"fake_audio_data",
190
- format="wav",
191
- sample_rate=44100,
192
- duration=10.0,
193
- filename=""
194
- )
195
-
196
- def test_whitespace_filename_raises_error(self):
197
- """Test that whitespace-only filename raises ValueError."""
198
- with pytest.raises(ValueError, match="Filename cannot be empty string"):
199
- AudioContent(
200
- data=b"fake_audio_data",
201
- format="wav",
202
- sample_rate=44100,
203
- duration=10.0,
204
- filename=" "
205
- )
206
-
207
- def test_audio_content_is_immutable(self):
208
- """Test that AudioContent is immutable (frozen dataclass)."""
209
- audio = AudioContent(
210
- data=b"fake_audio_data",
211
- format="wav",
212
- sample_rate=44100,
213
- duration=10.0
214
- )
215
-
216
- with pytest.raises(AttributeError):
217
- audio.format = "mp3" # type: ignore
218
-
219
- def test_size_bytes_property(self):
220
- """Test size_bytes property returns correct value."""
221
- test_data = b"test_audio_data_123"
222
- audio = AudioContent(
223
- data=test_data,
224
- format="wav",
225
- sample_rate=44100,
226
- duration=10.0
227
- )
228
-
229
- assert audio.size_bytes == len(test_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/models/test_processing_result.py DELETED
@@ -1,411 +0,0 @@
1
- """Unit tests for ProcessingResult value object."""
2
-
3
- import pytest
4
- from src.domain.models.processing_result import ProcessingResult
5
- from src.domain.models.text_content import TextContent
6
- from src.domain.models.audio_content import AudioContent
7
-
8
-
9
- class TestProcessingResult:
10
- """Test cases for ProcessingResult value object."""
11
-
12
- @pytest.fixture
13
- def sample_text_content(self):
14
- """Sample text content for testing."""
15
- return TextContent(text="Hello, world!", language="en")
16
-
17
- @pytest.fixture
18
- def sample_translated_text(self):
19
- """Sample translated text content for testing."""
20
- return TextContent(text="Hola, mundo!", language="es")
21
-
22
- @pytest.fixture
23
- def sample_audio_content(self):
24
- """Sample audio content for testing."""
25
- return AudioContent(
26
- data=b"fake_audio_data",
27
- format="wav",
28
- sample_rate=22050,
29
- duration=5.0
30
- )
31
-
32
- def test_valid_successful_processing_result(self, sample_text_content, sample_translated_text, sample_audio_content):
33
- """Test creating valid successful ProcessingResult."""
34
- result = ProcessingResult(
35
- success=True,
36
- original_text=sample_text_content,
37
- translated_text=sample_translated_text,
38
- audio_output=sample_audio_content,
39
- error_message=None,
40
- processing_time=2.5
41
- )
42
-
43
- assert result.success is True
44
- assert result.original_text == sample_text_content
45
- assert result.translated_text == sample_translated_text
46
- assert result.audio_output == sample_audio_content
47
- assert result.error_message is None
48
- assert result.processing_time == 2.5
49
- assert result.has_translation is True
50
- assert result.has_audio_output is True
51
- assert result.is_complete_pipeline is True
52
-
53
- def test_valid_failed_processing_result(self):
54
- """Test creating valid failed ProcessingResult."""
55
- result = ProcessingResult(
56
- success=False,
57
- original_text=None,
58
- translated_text=None,
59
- audio_output=None,
60
- error_message="Processing failed",
61
- processing_time=1.0
62
- )
63
-
64
- assert result.success is False
65
- assert result.original_text is None
66
- assert result.translated_text is None
67
- assert result.audio_output is None
68
- assert result.error_message == "Processing failed"
69
- assert result.processing_time == 1.0
70
- assert result.has_translation is False
71
- assert result.has_audio_output is False
72
- assert result.is_complete_pipeline is False
73
-
74
- def test_non_boolean_success_raises_error(self, sample_text_content):
75
- """Test that non-boolean success raises TypeError."""
76
- with pytest.raises(TypeError, match="Success must be a boolean"):
77
- ProcessingResult(
78
- success="true", # type: ignore
79
- original_text=sample_text_content,
80
- translated_text=None,
81
- audio_output=None,
82
- error_message=None,
83
- processing_time=1.0
84
- )
85
-
86
- def test_invalid_original_text_type_raises_error(self):
87
- """Test that invalid original text type raises TypeError."""
88
- with pytest.raises(TypeError, match="Original text must be a TextContent instance or None"):
89
- ProcessingResult(
90
- success=True,
91
- original_text="not a TextContent", # type: ignore
92
- translated_text=None,
93
- audio_output=None,
94
- error_message=None,
95
- processing_time=1.0
96
- )
97
-
98
- def test_invalid_translated_text_type_raises_error(self, sample_text_content):
99
- """Test that invalid translated text type raises TypeError."""
100
- with pytest.raises(TypeError, match="Translated text must be a TextContent instance or None"):
101
- ProcessingResult(
102
- success=True,
103
- original_text=sample_text_content,
104
- translated_text="not a TextContent", # type: ignore
105
- audio_output=None,
106
- error_message=None,
107
- processing_time=1.0
108
- )
109
-
110
- def test_invalid_audio_output_type_raises_error(self, sample_text_content):
111
- """Test that invalid audio output type raises TypeError."""
112
- with pytest.raises(TypeError, match="Audio output must be an AudioContent instance or None"):
113
- ProcessingResult(
114
- success=True,
115
- original_text=sample_text_content,
116
- translated_text=None,
117
- audio_output="not an AudioContent", # type: ignore
118
- error_message=None,
119
- processing_time=1.0
120
- )
121
-
122
- def test_invalid_error_message_type_raises_error(self, sample_text_content):
123
- """Test that invalid error message type raises TypeError."""
124
- with pytest.raises(TypeError, match="Error message must be a string or None"):
125
- ProcessingResult(
126
- success=True,
127
- original_text=sample_text_content,
128
- translated_text=None,
129
- audio_output=None,
130
- error_message=123, # type: ignore
131
- processing_time=1.0
132
- )
133
-
134
- def test_non_numeric_processing_time_raises_error(self, sample_text_content):
135
- """Test that non-numeric processing time raises TypeError."""
136
- with pytest.raises(TypeError, match="Processing time must be a number"):
137
- ProcessingResult(
138
- success=True,
139
- original_text=sample_text_content,
140
- translated_text=None,
141
- audio_output=None,
142
- error_message=None,
143
- processing_time="1.0" # type: ignore
144
- )
145
-
146
- def test_negative_processing_time_raises_error(self, sample_text_content):
147
- """Test that negative processing time raises ValueError."""
148
- with pytest.raises(ValueError, match="Processing time cannot be negative"):
149
- ProcessingResult(
150
- success=True,
151
- original_text=sample_text_content,
152
- translated_text=None,
153
- audio_output=None,
154
- error_message=None,
155
- processing_time=-1.0
156
- )
157
-
158
- def test_successful_result_with_error_message_raises_error(self, sample_text_content):
159
- """Test that successful result with error message raises ValueError."""
160
- with pytest.raises(ValueError, match="Successful result cannot have an error message"):
161
- ProcessingResult(
162
- success=True,
163
- original_text=sample_text_content,
164
- translated_text=None,
165
- audio_output=None,
166
- error_message="This should not be here",
167
- processing_time=1.0
168
- )
169
-
170
- def test_successful_result_without_original_text_raises_error(self):
171
- """Test that successful result without original text raises ValueError."""
172
- with pytest.raises(ValueError, match="Successful result must have original text"):
173
- ProcessingResult(
174
- success=True,
175
- original_text=None,
176
- translated_text=None,
177
- audio_output=None,
178
- error_message=None,
179
- processing_time=1.0
180
- )
181
-
182
- def test_failed_result_without_error_message_raises_error(self):
183
- """Test that failed result without error message raises ValueError."""
184
- with pytest.raises(ValueError, match="Failed result must have a non-empty error message"):
185
- ProcessingResult(
186
- success=False,
187
- original_text=None,
188
- translated_text=None,
189
- audio_output=None,
190
- error_message=None,
191
- processing_time=1.0
192
- )
193
-
194
- def test_failed_result_with_empty_error_message_raises_error(self):
195
- """Test that failed result with empty error message raises ValueError."""
196
- with pytest.raises(ValueError, match="Failed result must have a non-empty error message"):
197
- ProcessingResult(
198
- success=False,
199
- original_text=None,
200
- translated_text=None,
201
- audio_output=None,
202
- error_message="",
203
- processing_time=1.0
204
- )
205
-
206
- def test_failed_result_with_whitespace_error_message_raises_error(self):
207
- """Test that failed result with whitespace-only error message raises ValueError."""
208
- with pytest.raises(ValueError, match="Failed result must have a non-empty error message"):
209
- ProcessingResult(
210
- success=False,
211
- original_text=None,
212
- translated_text=None,
213
- audio_output=None,
214
- error_message=" ",
215
- processing_time=1.0
216
- )
217
-
218
- def test_has_translation_property(self, sample_text_content, sample_translated_text):
219
- """Test has_translation property."""
220
- # With translation
221
- result_with_translation = ProcessingResult(
222
- success=True,
223
- original_text=sample_text_content,
224
- translated_text=sample_translated_text,
225
- audio_output=None,
226
- error_message=None,
227
- processing_time=1.0
228
- )
229
- assert result_with_translation.has_translation is True
230
-
231
- # Without translation
232
- result_without_translation = ProcessingResult(
233
- success=True,
234
- original_text=sample_text_content,
235
- translated_text=None,
236
- audio_output=None,
237
- error_message=None,
238
- processing_time=1.0
239
- )
240
- assert result_without_translation.has_translation is False
241
-
242
- def test_has_audio_output_property(self, sample_text_content, sample_audio_content):
243
- """Test has_audio_output property."""
244
- # With audio output
245
- result_with_audio = ProcessingResult(
246
- success=True,
247
- original_text=sample_text_content,
248
- translated_text=None,
249
- audio_output=sample_audio_content,
250
- error_message=None,
251
- processing_time=1.0
252
- )
253
- assert result_with_audio.has_audio_output is True
254
-
255
- # Without audio output
256
- result_without_audio = ProcessingResult(
257
- success=True,
258
- original_text=sample_text_content,
259
- translated_text=None,
260
- audio_output=None,
261
- error_message=None,
262
- processing_time=1.0
263
- )
264
- assert result_without_audio.has_audio_output is False
265
-
266
- def test_is_complete_pipeline_property(self, sample_text_content, sample_translated_text, sample_audio_content):
267
- """Test is_complete_pipeline property."""
268
- # Complete pipeline
269
- complete_result = ProcessingResult(
270
- success=True,
271
- original_text=sample_text_content,
272
- translated_text=sample_translated_text,
273
- audio_output=sample_audio_content,
274
- error_message=None,
275
- processing_time=1.0
276
- )
277
- assert complete_result.is_complete_pipeline is True
278
-
279
- # Incomplete pipeline (missing translation)
280
- incomplete_result = ProcessingResult(
281
- success=True,
282
- original_text=sample_text_content,
283
- translated_text=None,
284
- audio_output=sample_audio_content,
285
- error_message=None,
286
- processing_time=1.0
287
- )
288
- assert incomplete_result.is_complete_pipeline is False
289
-
290
- # Failed result
291
- failed_result = ProcessingResult(
292
- success=False,
293
- original_text=None,
294
- translated_text=None,
295
- audio_output=None,
296
- error_message="Failed",
297
- processing_time=1.0
298
- )
299
- assert failed_result.is_complete_pipeline is False
300
-
301
- def test_success_result_class_method(self, sample_text_content, sample_translated_text, sample_audio_content):
302
- """Test success_result class method."""
303
- result = ProcessingResult.success_result(
304
- original_text=sample_text_content,
305
- translated_text=sample_translated_text,
306
- audio_output=sample_audio_content,
307
- processing_time=2.5
308
- )
309
-
310
- assert result.success is True
311
- assert result.original_text == sample_text_content
312
- assert result.translated_text == sample_translated_text
313
- assert result.audio_output == sample_audio_content
314
- assert result.error_message is None
315
- assert result.processing_time == 2.5
316
-
317
- def test_success_result_with_minimal_parameters(self, sample_text_content):
318
- """Test success_result class method with minimal parameters."""
319
- result = ProcessingResult.success_result(
320
- original_text=sample_text_content
321
- )
322
-
323
- assert result.success is True
324
- assert result.original_text == sample_text_content
325
- assert result.translated_text is None
326
- assert result.audio_output is None
327
- assert result.error_message is None
328
- assert result.processing_time == 0.0
329
-
330
- def test_failure_result_class_method(self, sample_text_content):
331
- """Test failure_result class method."""
332
- result = ProcessingResult.failure_result(
333
- error_message="Something went wrong",
334
- processing_time=1.5,
335
- original_text=sample_text_content
336
- )
337
-
338
- assert result.success is False
339
- assert result.original_text == sample_text_content
340
- assert result.translated_text is None
341
- assert result.audio_output is None
342
- assert result.error_message == "Something went wrong"
343
- assert result.processing_time == 1.5
344
-
345
- def test_failure_result_with_minimal_parameters(self):
346
- """Test failure_result class method with minimal parameters."""
347
- result = ProcessingResult.failure_result(
348
- error_message="Failed"
349
- )
350
-
351
- assert result.success is False
352
- assert result.original_text is None
353
- assert result.translated_text is None
354
- assert result.audio_output is None
355
- assert result.error_message == "Failed"
356
- assert result.processing_time == 0.0
357
-
358
- def test_processing_result_is_immutable(self, sample_text_content):
359
- """Test that ProcessingResult is immutable (frozen dataclass)."""
360
- result = ProcessingResult(
361
- success=True,
362
- original_text=sample_text_content,
363
- translated_text=None,
364
- audio_output=None,
365
- error_message=None,
366
- processing_time=1.0
367
- )
368
-
369
- with pytest.raises(AttributeError):
370
- result.success = False # type: ignore
371
-
372
- def test_zero_processing_time_valid(self, sample_text_content):
373
- """Test that zero processing time is valid."""
374
- result = ProcessingResult(
375
- success=True,
376
- original_text=sample_text_content,
377
- translated_text=None,
378
- audio_output=None,
379
- error_message=None,
380
- processing_time=0.0
381
- )
382
-
383
- assert result.processing_time == 0.0
384
-
385
- def test_partial_success_scenarios(self, sample_text_content, sample_translated_text):
386
- """Test various partial success scenarios."""
387
- # Only STT completed
388
- stt_only = ProcessingResult(
389
- success=True,
390
- original_text=sample_text_content,
391
- translated_text=None,
392
- audio_output=None,
393
- error_message=None,
394
- processing_time=1.0
395
- )
396
- assert stt_only.has_translation is False
397
- assert stt_only.has_audio_output is False
398
- assert stt_only.is_complete_pipeline is False
399
-
400
- # STT + Translation completed
401
- stt_translation = ProcessingResult(
402
- success=True,
403
- original_text=sample_text_content,
404
- translated_text=sample_translated_text,
405
- audio_output=None,
406
- error_message=None,
407
- processing_time=1.5
408
- )
409
- assert stt_translation.has_translation is True
410
- assert stt_translation.has_audio_output is False
411
- assert stt_translation.is_complete_pipeline is False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/models/test_speech_synthesis_request.py DELETED
@@ -1,323 +0,0 @@
1
- """Unit tests for SpeechSynthesisRequest value object."""
2
-
3
- import pytest
4
- from src.domain.models.speech_synthesis_request import SpeechSynthesisRequest
5
- from src.domain.models.text_content import TextContent
6
- from src.domain.models.voice_settings import VoiceSettings
7
-
8
-
9
- class TestSpeechSynthesisRequest:
10
- """Test cases for SpeechSynthesisRequest value object."""
11
-
12
- @pytest.fixture
13
- def sample_text_content(self):
14
- """Sample text content for testing."""
15
- return TextContent(
16
- text="Hello, world!",
17
- language="en"
18
- )
19
-
20
- @pytest.fixture
21
- def sample_voice_settings(self):
22
- """Sample voice settings for testing."""
23
- return VoiceSettings(
24
- voice_id="en_male_001",
25
- speed=1.0,
26
- language="en"
27
- )
28
-
29
- def test_valid_speech_synthesis_request_creation(self, sample_text_content, sample_voice_settings):
30
- """Test creating valid SpeechSynthesisRequest instance."""
31
- request = SpeechSynthesisRequest(
32
- text_content=sample_text_content,
33
- voice_settings=sample_voice_settings,
34
- output_format="wav",
35
- sample_rate=22050
36
- )
37
-
38
- assert request.text_content == sample_text_content
39
- assert request.voice_settings == sample_voice_settings
40
- assert request.output_format == "wav"
41
- assert request.sample_rate == 22050
42
- assert request.effective_sample_rate == 22050
43
-
44
- def test_speech_synthesis_request_with_defaults(self, sample_text_content, sample_voice_settings):
45
- """Test creating SpeechSynthesisRequest with default values."""
46
- request = SpeechSynthesisRequest(
47
- text_content=sample_text_content,
48
- voice_settings=sample_voice_settings
49
- )
50
-
51
- assert request.output_format == "wav"
52
- assert request.sample_rate is None
53
- assert request.effective_sample_rate == 22050 # Default
54
-
55
- def test_non_text_content_raises_error(self, sample_voice_settings):
56
- """Test that non-TextContent raises TypeError."""
57
- with pytest.raises(TypeError, match="Text must be a TextContent instance"):
58
- SpeechSynthesisRequest(
59
- text_content="not a TextContent", # type: ignore
60
- voice_settings=sample_voice_settings
61
- )
62
-
63
- def test_non_voice_settings_raises_error(self, sample_text_content):
64
- """Test that non-VoiceSettings raises TypeError."""
65
- with pytest.raises(TypeError, match="Voice settings must be a VoiceSettings instance"):
66
- SpeechSynthesisRequest(
67
- text_content=sample_text_content,
68
- voice_settings="not voice settings" # type: ignore
69
- )
70
-
71
- def test_non_string_output_format_raises_error(self, sample_text_content, sample_voice_settings):
72
- """Test that non-string output format raises TypeError."""
73
- with pytest.raises(TypeError, match="Output format must be a string"):
74
- SpeechSynthesisRequest(
75
- text_content=sample_text_content,
76
- voice_settings=sample_voice_settings,
77
- output_format=123 # type: ignore
78
- )
79
-
80
- def test_unsupported_output_format_raises_error(self, sample_text_content, sample_voice_settings):
81
- """Test that unsupported output format raises ValueError."""
82
- with pytest.raises(ValueError, match="Unsupported output format: xyz"):
83
- SpeechSynthesisRequest(
84
- text_content=sample_text_content,
85
- voice_settings=sample_voice_settings,
86
- output_format="xyz"
87
- )
88
-
89
- def test_supported_output_formats(self, sample_text_content, sample_voice_settings):
90
- """Test all supported output formats."""
91
- supported_formats = ['wav', 'mp3', 'flac', 'ogg']
92
-
93
- for fmt in supported_formats:
94
- request = SpeechSynthesisRequest(
95
- text_content=sample_text_content,
96
- voice_settings=sample_voice_settings,
97
- output_format=fmt
98
- )
99
- assert request.output_format == fmt
100
-
101
- def test_non_integer_sample_rate_raises_error(self, sample_text_content, sample_voice_settings):
102
- """Test that non-integer sample rate raises TypeError."""
103
- with pytest.raises(TypeError, match="Sample rate must be an integer"):
104
- SpeechSynthesisRequest(
105
- text_content=sample_text_content,
106
- voice_settings=sample_voice_settings,
107
- sample_rate=22050.5 # type: ignore
108
- )
109
-
110
- def test_negative_sample_rate_raises_error(self, sample_text_content, sample_voice_settings):
111
- """Test that negative sample rate raises ValueError."""
112
- with pytest.raises(ValueError, match="Sample rate must be positive"):
113
- SpeechSynthesisRequest(
114
- text_content=sample_text_content,
115
- voice_settings=sample_voice_settings,
116
- sample_rate=-1
117
- )
118
-
119
- def test_zero_sample_rate_raises_error(self, sample_text_content, sample_voice_settings):
120
- """Test that zero sample rate raises ValueError."""
121
- with pytest.raises(ValueError, match="Sample rate must be positive"):
122
- SpeechSynthesisRequest(
123
- text_content=sample_text_content,
124
- voice_settings=sample_voice_settings,
125
- sample_rate=0
126
- )
127
-
128
- def test_sample_rate_too_low_raises_error(self, sample_text_content, sample_voice_settings):
129
- """Test that sample rate below 8000 raises ValueError."""
130
- with pytest.raises(ValueError, match="Sample rate must be between 8000 and 192000 Hz"):
131
- SpeechSynthesisRequest(
132
- text_content=sample_text_content,
133
- voice_settings=sample_voice_settings,
134
- sample_rate=7999
135
- )
136
-
137
- def test_sample_rate_too_high_raises_error(self, sample_text_content, sample_voice_settings):
138
- """Test that sample rate above 192000 raises ValueError."""
139
- with pytest.raises(ValueError, match="Sample rate must be between 8000 and 192000 Hz"):
140
- SpeechSynthesisRequest(
141
- text_content=sample_text_content,
142
- voice_settings=sample_voice_settings,
143
- sample_rate=192001
144
- )
145
-
146
- def test_valid_sample_rate_boundaries(self, sample_text_content, sample_voice_settings):
147
- """Test valid sample rate boundaries."""
148
- # Test minimum valid sample rate
149
- request_min = SpeechSynthesisRequest(
150
- text_content=sample_text_content,
151
- voice_settings=sample_voice_settings,
152
- sample_rate=8000
153
- )
154
- assert request_min.sample_rate == 8000
155
-
156
- # Test maximum valid sample rate
157
- request_max = SpeechSynthesisRequest(
158
- text_content=sample_text_content,
159
- voice_settings=sample_voice_settings,
160
- sample_rate=192000
161
- )
162
- assert request_max.sample_rate == 192000
163
-
164
- def test_language_mismatch_raises_error(self, sample_voice_settings):
165
- """Test that language mismatch between text and voice raises ValueError."""
166
- text_content = TextContent(text="Hola mundo", language="es")
167
-
168
- with pytest.raises(ValueError, match="Text language \\(es\\) must match voice language \\(en\\)"):
169
- SpeechSynthesisRequest(
170
- text_content=text_content,
171
- voice_settings=sample_voice_settings # language="en"
172
- )
173
-
174
- def test_matching_languages_success(self):
175
- """Test that matching languages between text and voice works."""
176
- text_content = TextContent(text="Hola mundo", language="es")
177
- voice_settings = VoiceSettings(voice_id="es_female_001", speed=1.0, language="es")
178
-
179
- request = SpeechSynthesisRequest(
180
- text_content=text_content,
181
- voice_settings=voice_settings
182
- )
183
-
184
- assert request.text_content.language == request.voice_settings.language
185
-
186
- def test_estimated_duration_seconds_property(self, sample_text_content, sample_voice_settings):
187
- """Test estimated_duration_seconds property calculation."""
188
- request = SpeechSynthesisRequest(
189
- text_content=sample_text_content,
190
- voice_settings=sample_voice_settings
191
- )
192
-
193
- # Should be based on word count and speed
194
- expected_duration = (sample_text_content.word_count / (175 / sample_voice_settings.speed)) * 60
195
- assert abs(request.estimated_duration_seconds - expected_duration) < 0.01
196
-
197
- def test_estimated_duration_with_different_speed(self, sample_text_content):
198
- """Test estimated duration with different speech speed."""
199
- fast_voice = VoiceSettings(voice_id="test", speed=2.0, language="en")
200
- slow_voice = VoiceSettings(voice_id="test", speed=0.5, language="en")
201
-
202
- fast_request = SpeechSynthesisRequest(
203
- text_content=sample_text_content,
204
- voice_settings=fast_voice
205
- )
206
-
207
- slow_request = SpeechSynthesisRequest(
208
- text_content=sample_text_content,
209
- voice_settings=slow_voice
210
- )
211
-
212
- # Faster speed should result in shorter duration
213
- assert fast_request.estimated_duration_seconds < slow_request.estimated_duration_seconds
214
-
215
- def test_is_long_text_property(self, sample_voice_settings):
216
- """Test is_long_text property."""
217
- # Short text
218
- short_text = TextContent(text="Hello", language="en")
219
- short_request = SpeechSynthesisRequest(
220
- text_content=short_text,
221
- voice_settings=sample_voice_settings
222
- )
223
- assert short_request.is_long_text is False
224
-
225
- # Long text (over 5000 characters)
226
- long_text = TextContent(text="a" * 5001, language="en")
227
- long_request = SpeechSynthesisRequest(
228
- text_content=long_text,
229
- voice_settings=sample_voice_settings
230
- )
231
- assert long_request.is_long_text is True
232
-
233
- def test_with_output_format_method(self, sample_text_content, sample_voice_settings):
234
- """Test with_output_format method creates new instance."""
235
- original = SpeechSynthesisRequest(
236
- text_content=sample_text_content,
237
- voice_settings=sample_voice_settings,
238
- output_format="wav",
239
- sample_rate=22050
240
- )
241
-
242
- new_request = original.with_output_format("mp3")
243
-
244
- assert new_request.output_format == "mp3"
245
- assert new_request.text_content == original.text_content
246
- assert new_request.voice_settings == original.voice_settings
247
- assert new_request.sample_rate == original.sample_rate
248
- assert new_request is not original # Different instances
249
-
250
- def test_with_sample_rate_method(self, sample_text_content, sample_voice_settings):
251
- """Test with_sample_rate method creates new instance."""
252
- original = SpeechSynthesisRequest(
253
- text_content=sample_text_content,
254
- voice_settings=sample_voice_settings,
255
- sample_rate=22050
256
- )
257
-
258
- new_request = original.with_sample_rate(44100)
259
-
260
- assert new_request.sample_rate == 44100
261
- assert new_request.text_content == original.text_content
262
- assert new_request.voice_settings == original.voice_settings
263
- assert new_request.output_format == original.output_format
264
- assert new_request is not original # Different instances
265
-
266
- def test_with_sample_rate_none(self, sample_text_content, sample_voice_settings):
267
- """Test with_sample_rate method with None value."""
268
- original = SpeechSynthesisRequest(
269
- text_content=sample_text_content,
270
- voice_settings=sample_voice_settings,
271
- sample_rate=22050
272
- )
273
-
274
- new_request = original.with_sample_rate(None)
275
- assert new_request.sample_rate is None
276
- assert new_request.effective_sample_rate == 22050 # Default
277
-
278
- def test_with_voice_settings_method(self, sample_text_content, sample_voice_settings):
279
- """Test with_voice_settings method creates new instance."""
280
- new_voice_settings = VoiceSettings(voice_id="en_female_001", speed=1.5, language="en")
281
-
282
- original = SpeechSynthesisRequest(
283
- text_content=sample_text_content,
284
- voice_settings=sample_voice_settings
285
- )
286
-
287
- new_request = original.with_voice_settings(new_voice_settings)
288
-
289
- assert new_request.voice_settings == new_voice_settings
290
- assert new_request.text_content == original.text_content
291
- assert new_request.output_format == original.output_format
292
- assert new_request.sample_rate == original.sample_rate
293
- assert new_request is not original # Different instances
294
-
295
- def test_speech_synthesis_request_is_immutable(self, sample_text_content, sample_voice_settings):
296
- """Test that SpeechSynthesisRequest is immutable (frozen dataclass)."""
297
- request = SpeechSynthesisRequest(
298
- text_content=sample_text_content,
299
- voice_settings=sample_voice_settings
300
- )
301
-
302
- with pytest.raises(AttributeError):
303
- request.output_format = "mp3" # type: ignore
304
-
305
- def test_effective_sample_rate_with_none(self, sample_text_content, sample_voice_settings):
306
- """Test effective_sample_rate when sample_rate is None."""
307
- request = SpeechSynthesisRequest(
308
- text_content=sample_text_content,
309
- voice_settings=sample_voice_settings,
310
- sample_rate=None
311
- )
312
-
313
- assert request.effective_sample_rate == 22050 # Default value
314
-
315
- def test_effective_sample_rate_with_value(self, sample_text_content, sample_voice_settings):
316
- """Test effective_sample_rate when sample_rate has a value."""
317
- request = SpeechSynthesisRequest(
318
- text_content=sample_text_content,
319
- voice_settings=sample_voice_settings,
320
- sample_rate=44100
321
- )
322
-
323
- assert request.effective_sample_rate == 44100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/models/test_text_content.py DELETED
@@ -1,240 +0,0 @@
1
- """Unit tests for TextContent value object."""
2
-
3
- import pytest
4
- from src.domain.models.text_content import TextContent
5
-
6
-
7
- class TestTextContent:
8
- """Test cases for TextContent value object."""
9
-
10
- def test_valid_text_content_creation(self):
11
- """Test creating valid TextContent instance."""
12
- text = TextContent(
13
- text="Hello, world!",
14
- language="en",
15
- encoding="utf-8"
16
- )
17
-
18
- assert text.text == "Hello, world!"
19
- assert text.language == "en"
20
- assert text.encoding == "utf-8"
21
- assert text.word_count == 2
22
- assert text.character_count == 13
23
- assert text.is_empty is False
24
-
25
- def test_text_content_with_default_encoding(self):
26
- """Test creating TextContent with default encoding."""
27
- text = TextContent(
28
- text="Hello, world!",
29
- language="en"
30
- )
31
-
32
- assert text.encoding == "utf-8"
33
-
34
- def test_non_string_text_raises_error(self):
35
- """Test that non-string text raises TypeError."""
36
- with pytest.raises(TypeError, match="Text must be a string"):
37
- TextContent(
38
- text=123, # type: ignore
39
- language="en"
40
- )
41
-
42
- def test_empty_text_raises_error(self):
43
- """Test that empty text raises ValueError."""
44
- with pytest.raises(ValueError, match="Text content cannot be empty or whitespace only"):
45
- TextContent(
46
- text="",
47
- language="en"
48
- )
49
-
50
- def test_whitespace_only_text_raises_error(self):
51
- """Test that whitespace-only text raises ValueError."""
52
- with pytest.raises(ValueError, match="Text content cannot be empty or whitespace only"):
53
- TextContent(
54
- text=" \n\t ",
55
- language="en"
56
- )
57
-
58
- def test_text_too_long_raises_error(self):
59
- """Test that text over 50,000 characters raises ValueError."""
60
- long_text = "a" * 50001
61
- with pytest.raises(ValueError, match="Text content too long"):
62
- TextContent(
63
- text=long_text,
64
- language="en"
65
- )
66
-
67
- def test_text_at_max_length(self):
68
- """Test text at maximum allowed length."""
69
- max_text = "a" * 50000
70
- text = TextContent(
71
- text=max_text,
72
- language="en"
73
- )
74
- assert len(text.text) == 50000
75
-
76
- def test_non_string_language_raises_error(self):
77
- """Test that non-string language raises TypeError."""
78
- with pytest.raises(TypeError, match="Language must be a string"):
79
- TextContent(
80
- text="Hello",
81
- language=123 # type: ignore
82
- )
83
-
84
- def test_empty_language_raises_error(self):
85
- """Test that empty language raises ValueError."""
86
- with pytest.raises(ValueError, match="Language cannot be empty"):
87
- TextContent(
88
- text="Hello",
89
- language=""
90
- )
91
-
92
- def test_whitespace_language_raises_error(self):
93
- """Test that whitespace-only language raises ValueError."""
94
- with pytest.raises(ValueError, match="Language cannot be empty"):
95
- TextContent(
96
- text="Hello",
97
- language=" "
98
- )
99
-
100
- def test_invalid_language_code_format_raises_error(self):
101
- """Test that invalid language code format raises ValueError."""
102
- invalid_codes = ["e", "ENG", "en-us", "en-USA", "123", "en_US"]
103
-
104
- for code in invalid_codes:
105
- with pytest.raises(ValueError, match="Invalid language code format"):
106
- TextContent(
107
- text="Hello",
108
- language=code
109
- )
110
-
111
- def test_valid_language_codes(self):
112
- """Test valid language code formats."""
113
- valid_codes = ["en", "fr", "de", "es", "zh", "ja", "en-US", "fr-FR", "zh-CN"]
114
-
115
- for code in valid_codes:
116
- text = TextContent(
117
- text="Hello",
118
- language=code
119
- )
120
- assert text.language == code
121
-
122
- def test_non_string_encoding_raises_error(self):
123
- """Test that non-string encoding raises TypeError."""
124
- with pytest.raises(TypeError, match="Encoding must be a string"):
125
- TextContent(
126
- text="Hello",
127
- language="en",
128
- encoding=123 # type: ignore
129
- )
130
-
131
- def test_unsupported_encoding_raises_error(self):
132
- """Test that unsupported encoding raises ValueError."""
133
- with pytest.raises(ValueError, match="Unsupported encoding: xyz"):
134
- TextContent(
135
- text="Hello",
136
- language="en",
137
- encoding="xyz"
138
- )
139
-
140
- def test_supported_encodings(self):
141
- """Test all supported encodings."""
142
- supported_encodings = ['utf-8', 'utf-16', 'ascii', 'latin-1']
143
-
144
- for encoding in supported_encodings:
145
- text = TextContent(
146
- text="Hello",
147
- language="en",
148
- encoding=encoding
149
- )
150
- assert text.encoding == encoding
151
-
152
- def test_text_encoding_compatibility(self):
153
- """Test that text is compatible with specified encoding."""
154
- # ASCII text with UTF-8 encoding should work
155
- text = TextContent(
156
- text="Hello",
157
- language="en",
158
- encoding="ascii"
159
- )
160
- assert text.encoding == "ascii"
161
-
162
- # Unicode text with ASCII encoding should fail
163
- with pytest.raises(ValueError, match="Text cannot be encoded with ascii encoding"):
164
- TextContent(
165
- text="Héllo", # Contains non-ASCII character
166
- language="en",
167
- encoding="ascii"
168
- )
169
-
170
- def test_word_count_property(self):
171
- """Test word_count property calculation."""
172
- test_cases = [
173
- ("Hello world", 2),
174
- ("Hello", 1),
175
- ("Hello world test", 3),
176
- ("Hello, world! Test.", 3), # Multiple spaces and punctuation
177
- ("", 1), # Empty string split returns ['']
178
- ]
179
-
180
- for text_str, expected_count in test_cases:
181
- if text_str: # Skip empty string test as it would fail validation
182
- text = TextContent(text=text_str, language="en")
183
- assert text.word_count == expected_count
184
-
185
- def test_character_count_property(self):
186
- """Test character_count property."""
187
- text_str = "Hello, world!"
188
- text = TextContent(text=text_str, language="en")
189
- assert text.character_count == len(text_str)
190
-
191
- def test_is_empty_property(self):
192
- """Test is_empty property."""
193
- # Non-empty text
194
- text = TextContent(text="Hello", language="en")
195
- assert text.is_empty is False
196
-
197
- # Text with only meaningful content
198
- text2 = TextContent(text=" Hello ", language="en")
199
- assert text2.is_empty is False
200
-
201
- def test_truncate_method(self):
202
- """Test truncate method."""
203
- text = TextContent(text="Hello, world! This is a test.", language="en")
204
-
205
- # Truncate to shorter length
206
- truncated = text.truncate(10)
207
- assert len(truncated.text) <= 10
208
- assert truncated.language == text.language
209
- assert truncated.encoding == text.encoding
210
- assert isinstance(truncated, TextContent)
211
-
212
- # Truncate to longer length (should return same)
213
- not_truncated = text.truncate(100)
214
- assert not_truncated.text == text.text
215
-
216
- def test_truncate_with_invalid_length(self):
217
- """Test truncate with invalid max_length."""
218
- text = TextContent(text="Hello", language="en")
219
-
220
- with pytest.raises(ValueError, match="Max length must be positive"):
221
- text.truncate(0)
222
-
223
- with pytest.raises(ValueError, match="Max length must be positive"):
224
- text.truncate(-1)
225
-
226
- def test_text_content_is_immutable(self):
227
- """Test that TextContent is immutable (frozen dataclass)."""
228
- text = TextContent(text="Hello", language="en")
229
-
230
- with pytest.raises(AttributeError):
231
- text.text = "Goodbye" # type: ignore
232
-
233
- def test_truncate_preserves_word_boundaries(self):
234
- """Test that truncate method preserves word boundaries by rstripping."""
235
- text = TextContent(text="Hello world test", language="en")
236
-
237
- # Truncate in middle of word
238
- truncated = text.truncate(12) # "Hello world " -> "Hello world" after rstrip
239
- assert not truncated.text.endswith(" ")
240
- assert truncated.text == "Hello world"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/models/test_translation_request.py DELETED
@@ -1,243 +0,0 @@
1
- """Unit tests for TranslationRequest value object."""
2
-
3
- import pytest
4
- from src.domain.models.translation_request import TranslationRequest
5
- from src.domain.models.text_content import TextContent
6
-
7
-
8
- class TestTranslationRequest:
9
- """Test cases for TranslationRequest value object."""
10
-
11
- @pytest.fixture
12
- def sample_text_content(self):
13
- """Sample text content for testing."""
14
- return TextContent(
15
- text="Hello, world!",
16
- language="en"
17
- )
18
-
19
- def test_valid_translation_request_creation(self, sample_text_content):
20
- """Test creating valid TranslationRequest instance."""
21
- request = TranslationRequest(
22
- source_text=sample_text_content,
23
- target_language="es",
24
- source_language="en"
25
- )
26
-
27
- assert request.source_text == sample_text_content
28
- assert request.target_language == "es"
29
- assert request.source_language == "en"
30
- assert request.effective_source_language == "en"
31
- assert request.is_auto_detect_source is False
32
-
33
- def test_translation_request_without_source_language(self, sample_text_content):
34
- """Test creating TranslationRequest without explicit source language."""
35
- request = TranslationRequest(
36
- source_text=sample_text_content,
37
- target_language="es"
38
- )
39
-
40
- assert request.source_language is None
41
- assert request.effective_source_language == "en" # From TextContent
42
- assert request.is_auto_detect_source is True
43
-
44
- def test_non_text_content_source_raises_error(self):
45
- """Test that non-TextContent source raises TypeError."""
46
- with pytest.raises(TypeError, match="Source text must be a TextContent instance"):
47
- TranslationRequest(
48
- source_text="not a TextContent", # type: ignore
49
- target_language="es"
50
- )
51
-
52
- def test_non_string_target_language_raises_error(self, sample_text_content):
53
- """Test that non-string target language raises TypeError."""
54
- with pytest.raises(TypeError, match="Target language must be a string"):
55
- TranslationRequest(
56
- source_text=sample_text_content,
57
- target_language=123 # type: ignore
58
- )
59
-
60
- def test_empty_target_language_raises_error(self, sample_text_content):
61
- """Test that empty target language raises ValueError."""
62
- with pytest.raises(ValueError, match="Target language cannot be empty"):
63
- TranslationRequest(
64
- source_text=sample_text_content,
65
- target_language=""
66
- )
67
-
68
- def test_whitespace_target_language_raises_error(self, sample_text_content):
69
- """Test that whitespace-only target language raises ValueError."""
70
- with pytest.raises(ValueError, match="Target language cannot be empty"):
71
- TranslationRequest(
72
- source_text=sample_text_content,
73
- target_language=" "
74
- )
75
-
76
- def test_invalid_target_language_format_raises_error(self, sample_text_content):
77
- """Test that invalid target language format raises ValueError."""
78
- invalid_codes = ["e", "ENG", "en-us", "en-USA", "123", "en_US"]
79
-
80
- for code in invalid_codes:
81
- with pytest.raises(ValueError, match="Invalid target language code format"):
82
- TranslationRequest(
83
- source_text=sample_text_content,
84
- target_language=code
85
- )
86
-
87
- def test_valid_target_language_codes(self, sample_text_content):
88
- """Test valid target language code formats."""
89
- valid_codes = ["es", "fr", "de", "zh", "ja", "en-US", "fr-FR", "zh-CN"]
90
-
91
- for code in valid_codes:
92
- request = TranslationRequest(
93
- source_text=sample_text_content,
94
- target_language=code
95
- )
96
- assert request.target_language == code
97
-
98
- def test_non_string_source_language_raises_error(self, sample_text_content):
99
- """Test that non-string source language raises TypeError."""
100
- with pytest.raises(TypeError, match="Source language must be a string"):
101
- TranslationRequest(
102
- source_text=sample_text_content,
103
- target_language="es",
104
- source_language=123 # type: ignore
105
- )
106
-
107
- def test_empty_source_language_raises_error(self, sample_text_content):
108
- """Test that empty source language raises ValueError."""
109
- with pytest.raises(ValueError, match="Source language cannot be empty string"):
110
- TranslationRequest(
111
- source_text=sample_text_content,
112
- target_language="es",
113
- source_language=""
114
- )
115
-
116
- def test_whitespace_source_language_raises_error(self, sample_text_content):
117
- """Test that whitespace-only source language raises ValueError."""
118
- with pytest.raises(ValueError, match="Source language cannot be empty string"):
119
- TranslationRequest(
120
- source_text=sample_text_content,
121
- target_language="es",
122
- source_language=" "
123
- )
124
-
125
- def test_invalid_source_language_format_raises_error(self, sample_text_content):
126
- """Test that invalid source language format raises ValueError."""
127
- invalid_codes = ["e", "ENG", "en-us", "en-USA", "123", "en_US"]
128
-
129
- for code in invalid_codes:
130
- with pytest.raises(ValueError, match="Invalid source language code format"):
131
- TranslationRequest(
132
- source_text=sample_text_content,
133
- target_language="es",
134
- source_language=code
135
- )
136
-
137
- def test_same_source_and_target_language_explicit_raises_error(self, sample_text_content):
138
- """Test that same explicit source and target language raises ValueError."""
139
- with pytest.raises(ValueError, match="Source and target languages cannot be the same"):
140
- TranslationRequest(
141
- source_text=sample_text_content,
142
- target_language="en",
143
- source_language="en"
144
- )
145
-
146
- def test_same_source_and_target_language_implicit_raises_error(self):
147
- """Test that same implicit source and target language raises ValueError."""
148
- text_content = TextContent(text="Hello", language="en")
149
-
150
- with pytest.raises(ValueError, match="Source and target languages cannot be the same"):
151
- TranslationRequest(
152
- source_text=text_content,
153
- target_language="en" # Same as text_content.language
154
- )
155
-
156
- def test_text_length_property(self, sample_text_content):
157
- """Test text_length property."""
158
- request = TranslationRequest(
159
- source_text=sample_text_content,
160
- target_language="es"
161
- )
162
-
163
- assert request.text_length == len(sample_text_content.text)
164
-
165
- def test_word_count_property(self, sample_text_content):
166
- """Test word_count property."""
167
- request = TranslationRequest(
168
- source_text=sample_text_content,
169
- target_language="es"
170
- )
171
-
172
- assert request.word_count == sample_text_content.word_count
173
-
174
- def test_with_target_language_method(self, sample_text_content):
175
- """Test with_target_language method creates new instance."""
176
- original = TranslationRequest(
177
- source_text=sample_text_content,
178
- target_language="es",
179
- source_language="en"
180
- )
181
-
182
- new_request = original.with_target_language("fr")
183
-
184
- assert new_request.target_language == "fr"
185
- assert new_request.source_text == original.source_text
186
- assert new_request.source_language == original.source_language
187
- assert new_request is not original # Different instances
188
-
189
- def test_with_source_language_method(self, sample_text_content):
190
- """Test with_source_language method creates new instance."""
191
- original = TranslationRequest(
192
- source_text=sample_text_content,
193
- target_language="es",
194
- source_language="en"
195
- )
196
-
197
- new_request = original.with_source_language("de")
198
-
199
- assert new_request.source_language == "de"
200
- assert new_request.target_language == original.target_language
201
- assert new_request.source_text == original.source_text
202
- assert new_request is not original # Different instances
203
-
204
- def test_with_source_language_none(self, sample_text_content):
205
- """Test with_source_language method with None value."""
206
- original = TranslationRequest(
207
- source_text=sample_text_content,
208
- target_language="es",
209
- source_language="en"
210
- )
211
-
212
- new_request = original.with_source_language(None)
213
- assert new_request.source_language is None
214
- assert new_request.is_auto_detect_source is True
215
-
216
- def test_translation_request_is_immutable(self, sample_text_content):
217
- """Test that TranslationRequest is immutable (frozen dataclass)."""
218
- request = TranslationRequest(
219
- source_text=sample_text_content,
220
- target_language="es"
221
- )
222
-
223
- with pytest.raises(AttributeError):
224
- request.target_language = "fr" # type: ignore
225
-
226
- def test_effective_source_language_with_explicit_source(self, sample_text_content):
227
- """Test effective_source_language with explicit source language."""
228
- request = TranslationRequest(
229
- source_text=sample_text_content,
230
- target_language="es",
231
- source_language="de" # Different from text_content.language
232
- )
233
-
234
- assert request.effective_source_language == "de"
235
-
236
- def test_effective_source_language_with_implicit_source(self, sample_text_content):
237
- """Test effective_source_language with implicit source language."""
238
- request = TranslationRequest(
239
- source_text=sample_text_content,
240
- target_language="es"
241
- )
242
-
243
- assert request.effective_source_language == sample_text_content.language
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/models/test_voice_settings.py DELETED
@@ -1,408 +0,0 @@
1
- """Unit tests for VoiceSettings value object."""
2
-
3
- import pytest
4
- from src.domain.models.voice_settings import VoiceSettings
5
-
6
-
7
- class TestVoiceSettings:
8
- """Test cases for VoiceSettings value object."""
9
-
10
- def test_valid_voice_settings_creation(self):
11
- """Test creating valid VoiceSettings instance."""
12
- settings = VoiceSettings(
13
- voice_id="en_male_001",
14
- speed=1.2,
15
- language="en",
16
- pitch=0.1,
17
- volume=0.8
18
- )
19
-
20
- assert settings.voice_id == "en_male_001"
21
- assert settings.speed == 1.2
22
- assert settings.language == "en"
23
- assert settings.pitch == 0.1
24
- assert settings.volume == 0.8
25
-
26
- def test_voice_settings_with_optional_none(self):
27
- """Test creating VoiceSettings with optional parameters as None."""
28
- settings = VoiceSettings(
29
- voice_id="en_male_001",
30
- speed=1.0,
31
- language="en"
32
- )
33
-
34
- assert settings.pitch is None
35
- assert settings.volume is None
36
-
37
- def test_non_string_voice_id_raises_error(self):
38
- """Test that non-string voice_id raises TypeError."""
39
- with pytest.raises(TypeError, match="Voice ID must be a string"):
40
- VoiceSettings(
41
- voice_id=123, # type: ignore
42
- speed=1.0,
43
- language="en"
44
- )
45
-
46
- def test_empty_voice_id_raises_error(self):
47
- """Test that empty voice_id raises ValueError."""
48
- with pytest.raises(ValueError, match="Voice ID cannot be empty"):
49
- VoiceSettings(
50
- voice_id="",
51
- speed=1.0,
52
- language="en"
53
- )
54
-
55
- def test_whitespace_voice_id_raises_error(self):
56
- """Test that whitespace-only voice_id raises ValueError."""
57
- with pytest.raises(ValueError, match="Voice ID cannot be empty"):
58
- VoiceSettings(
59
- voice_id=" ",
60
- speed=1.0,
61
- language="en"
62
- )
63
-
64
- def test_invalid_voice_id_format_raises_error(self):
65
- """Test that invalid voice_id format raises ValueError."""
66
- invalid_ids = ["voice id", "voice@id", "voice.id", "voice/id", "voice\\id"]
67
-
68
- for voice_id in invalid_ids:
69
- with pytest.raises(ValueError, match="Invalid voice ID format"):
70
- VoiceSettings(
71
- voice_id=voice_id,
72
- speed=1.0,
73
- language="en"
74
- )
75
-
76
- def test_valid_voice_id_formats(self):
77
- """Test valid voice_id formats."""
78
- valid_ids = ["voice1", "voice_1", "voice-1", "en_male_001", "female-voice", "Voice123"]
79
-
80
- for voice_id in valid_ids:
81
- settings = VoiceSettings(
82
- voice_id=voice_id,
83
- speed=1.0,
84
- language="en"
85
- )
86
- assert settings.voice_id == voice_id
87
-
88
- def test_non_numeric_speed_raises_error(self):
89
- """Test that non-numeric speed raises TypeError."""
90
- with pytest.raises(TypeError, match="Speed must be a number"):
91
- VoiceSettings(
92
- voice_id="voice1",
93
- speed="fast", # type: ignore
94
- language="en"
95
- )
96
-
97
- def test_speed_too_low_raises_error(self):
98
- """Test that speed below 0.1 raises ValueError."""
99
- with pytest.raises(ValueError, match="Speed must be between 0.1 and 3.0"):
100
- VoiceSettings(
101
- voice_id="voice1",
102
- speed=0.05,
103
- language="en"
104
- )
105
-
106
- def test_speed_too_high_raises_error(self):
107
- """Test that speed above 3.0 raises ValueError."""
108
- with pytest.raises(ValueError, match="Speed must be between 0.1 and 3.0"):
109
- VoiceSettings(
110
- voice_id="voice1",
111
- speed=3.1,
112
- language="en"
113
- )
114
-
115
- def test_valid_speed_boundaries(self):
116
- """Test valid speed boundaries."""
117
- # Test minimum valid speed
118
- settings_min = VoiceSettings(
119
- voice_id="voice1",
120
- speed=0.1,
121
- language="en"
122
- )
123
- assert settings_min.speed == 0.1
124
-
125
- # Test maximum valid speed
126
- settings_max = VoiceSettings(
127
- voice_id="voice1",
128
- speed=3.0,
129
- language="en"
130
- )
131
- assert settings_max.speed == 3.0
132
-
133
- def test_non_string_language_raises_error(self):
134
- """Test that non-string language raises TypeError."""
135
- with pytest.raises(TypeError, match="Language must be a string"):
136
- VoiceSettings(
137
- voice_id="voice1",
138
- speed=1.0,
139
- language=123 # type: ignore
140
- )
141
-
142
- def test_empty_language_raises_error(self):
143
- """Test that empty language raises ValueError."""
144
- with pytest.raises(ValueError, match="Language cannot be empty"):
145
- VoiceSettings(
146
- voice_id="voice1",
147
- speed=1.0,
148
- language=""
149
- )
150
-
151
- def test_invalid_language_code_format_raises_error(self):
152
- """Test that invalid language code format raises ValueError."""
153
- invalid_codes = ["e", "ENG", "en-us", "en-USA", "123", "en_US"]
154
-
155
- for code in invalid_codes:
156
- with pytest.raises(ValueError, match="Invalid language code format"):
157
- VoiceSettings(
158
- voice_id="voice1",
159
- speed=1.0,
160
- language=code
161
- )
162
-
163
- def test_valid_language_codes(self):
164
- """Test valid language code formats."""
165
- valid_codes = ["en", "fr", "de", "es", "zh", "ja", "en-US", "fr-FR", "zh-CN"]
166
-
167
- for code in valid_codes:
168
- settings = VoiceSettings(
169
- voice_id="voice1",
170
- speed=1.0,
171
- language=code
172
- )
173
- assert settings.language == code
174
-
175
- def test_non_numeric_pitch_raises_error(self):
176
- """Test that non-numeric pitch raises TypeError."""
177
- with pytest.raises(TypeError, match="Pitch must be a number"):
178
- VoiceSettings(
179
- voice_id="voice1",
180
- speed=1.0,
181
- language="en",
182
- pitch="high" # type: ignore
183
- )
184
-
185
- def test_pitch_too_low_raises_error(self):
186
- """Test that pitch below -2.0 raises ValueError."""
187
- with pytest.raises(ValueError, match="Pitch must be between -2.0 and 2.0"):
188
- VoiceSettings(
189
- voice_id="voice1",
190
- speed=1.0,
191
- language="en",
192
- pitch=-2.1
193
- )
194
-
195
- def test_pitch_too_high_raises_error(self):
196
- """Test that pitch above 2.0 raises ValueError."""
197
- with pytest.raises(ValueError, match="Pitch must be between -2.0 and 2.0"):
198
- VoiceSettings(
199
- voice_id="voice1",
200
- speed=1.0,
201
- language="en",
202
- pitch=2.1
203
- )
204
-
205
- def test_valid_pitch_boundaries(self):
206
- """Test valid pitch boundaries."""
207
- # Test minimum valid pitch
208
- settings_min = VoiceSettings(
209
- voice_id="voice1",
210
- speed=1.0,
211
- language="en",
212
- pitch=-2.0
213
- )
214
- assert settings_min.pitch == -2.0
215
-
216
- # Test maximum valid pitch
217
- settings_max = VoiceSettings(
218
- voice_id="voice1",
219
- speed=1.0,
220
- language="en",
221
- pitch=2.0
222
- )
223
- assert settings_max.pitch == 2.0
224
-
225
- def test_non_numeric_volume_raises_error(self):
226
- """Test that non-numeric volume raises TypeError."""
227
- with pytest.raises(TypeError, match="Volume must be a number"):
228
- VoiceSettings(
229
- voice_id="voice1",
230
- speed=1.0,
231
- language="en",
232
- volume="loud" # type: ignore
233
- )
234
-
235
- def test_volume_too_low_raises_error(self):
236
- """Test that volume below 0.0 raises ValueError."""
237
- with pytest.raises(ValueError, match="Volume must be between 0.0 and 2.0"):
238
- VoiceSettings(
239
- voice_id="voice1",
240
- speed=1.0,
241
- language="en",
242
- volume=-0.1
243
- )
244
-
245
- def test_volume_too_high_raises_error(self):
246
- """Test that volume above 2.0 raises ValueError."""
247
- with pytest.raises(ValueError, match="Volume must be between 0.0 and 2.0"):
248
- VoiceSettings(
249
- voice_id="voice1",
250
- speed=1.0,
251
- language="en",
252
- volume=2.1
253
- )
254
-
255
- def test_valid_volume_boundaries(self):
256
- """Test valid volume boundaries."""
257
- # Test minimum valid volume
258
- settings_min = VoiceSettings(
259
- voice_id="voice1",
260
- speed=1.0,
261
- language="en",
262
- volume=0.0
263
- )
264
- assert settings_min.volume == 0.0
265
-
266
- # Test maximum valid volume
267
- settings_max = VoiceSettings(
268
- voice_id="voice1",
269
- speed=1.0,
270
- language="en",
271
- volume=2.0
272
- )
273
- assert settings_max.volume == 2.0
274
-
275
- def test_is_default_speed_property(self):
276
- """Test is_default_speed property."""
277
- # Default speed (1.0)
278
- settings_default = VoiceSettings(
279
- voice_id="voice1",
280
- speed=1.0,
281
- language="en"
282
- )
283
- assert settings_default.is_default_speed is True
284
-
285
- # Non-default speed
286
- settings_non_default = VoiceSettings(
287
- voice_id="voice1",
288
- speed=1.5,
289
- language="en"
290
- )
291
- assert settings_non_default.is_default_speed is False
292
-
293
- def test_is_default_pitch_property(self):
294
- """Test is_default_pitch property."""
295
- # Default pitch (None)
296
- settings_none = VoiceSettings(
297
- voice_id="voice1",
298
- speed=1.0,
299
- language="en"
300
- )
301
- assert settings_none.is_default_pitch is True
302
-
303
- # Default pitch (0.0)
304
- settings_zero = VoiceSettings(
305
- voice_id="voice1",
306
- speed=1.0,
307
- language="en",
308
- pitch=0.0
309
- )
310
- assert settings_zero.is_default_pitch is True
311
-
312
- # Non-default pitch
313
- settings_non_default = VoiceSettings(
314
- voice_id="voice1",
315
- speed=1.0,
316
- language="en",
317
- pitch=0.5
318
- )
319
- assert settings_non_default.is_default_pitch is False
320
-
321
- def test_is_default_volume_property(self):
322
- """Test is_default_volume property."""
323
- # Default volume (None)
324
- settings_none = VoiceSettings(
325
- voice_id="voice1",
326
- speed=1.0,
327
- language="en"
328
- )
329
- assert settings_none.is_default_volume is True
330
-
331
- # Default volume (1.0)
332
- settings_one = VoiceSettings(
333
- voice_id="voice1",
334
- speed=1.0,
335
- language="en",
336
- volume=1.0
337
- )
338
- assert settings_one.is_default_volume is True
339
-
340
- # Non-default volume
341
- settings_non_default = VoiceSettings(
342
- voice_id="voice1",
343
- speed=1.0,
344
- language="en",
345
- volume=0.5
346
- )
347
- assert settings_non_default.is_default_volume is False
348
-
349
- def test_with_speed_method(self):
350
- """Test with_speed method creates new instance."""
351
- original = VoiceSettings(
352
- voice_id="voice1",
353
- speed=1.0,
354
- language="en",
355
- pitch=0.1,
356
- volume=0.8
357
- )
358
-
359
- new_settings = original.with_speed(1.5)
360
-
361
- assert new_settings.speed == 1.5
362
- assert new_settings.voice_id == original.voice_id
363
- assert new_settings.language == original.language
364
- assert new_settings.pitch == original.pitch
365
- assert new_settings.volume == original.volume
366
- assert new_settings is not original # Different instances
367
-
368
- def test_with_pitch_method(self):
369
- """Test with_pitch method creates new instance."""
370
- original = VoiceSettings(
371
- voice_id="voice1",
372
- speed=1.0,
373
- language="en",
374
- pitch=0.1,
375
- volume=0.8
376
- )
377
-
378
- new_settings = original.with_pitch(0.5)
379
-
380
- assert new_settings.pitch == 0.5
381
- assert new_settings.voice_id == original.voice_id
382
- assert new_settings.speed == original.speed
383
- assert new_settings.language == original.language
384
- assert new_settings.volume == original.volume
385
- assert new_settings is not original # Different instances
386
-
387
- def test_with_pitch_none(self):
388
- """Test with_pitch method with None value."""
389
- original = VoiceSettings(
390
- voice_id="voice1",
391
- speed=1.0,
392
- language="en",
393
- pitch=0.1
394
- )
395
-
396
- new_settings = original.with_pitch(None)
397
- assert new_settings.pitch is None
398
-
399
- def test_voice_settings_is_immutable(self):
400
- """Test that VoiceSettings is immutable (frozen dataclass)."""
401
- settings = VoiceSettings(
402
- voice_id="voice1",
403
- speed=1.0,
404
- language="en"
405
- )
406
-
407
- with pytest.raises(AttributeError):
408
- settings.speed = 1.5 # type: ignore
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/services/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Domain services tests package."""
 
 
tests/unit/domain/services/test_audio_processing_service.py DELETED
@@ -1,297 +0,0 @@
1
- """Tests for AudioProcessingService."""
2
-
3
- import pytest
4
- from unittest.mock import Mock, MagicMock
5
- from src.domain.services.audio_processing_service import AudioProcessingService
6
- from src.domain.models.audio_content import AudioContent
7
- from src.domain.models.text_content import TextContent
8
- from src.domain.models.voice_settings import VoiceSettings
9
- from src.domain.models.translation_request import TranslationRequest
10
- from src.domain.models.speech_synthesis_request import SpeechSynthesisRequest
11
- from src.domain.exceptions import (
12
- AudioProcessingException,
13
- SpeechRecognitionException,
14
- TranslationFailedException,
15
- SpeechSynthesisException
16
- )
17
-
18
-
19
- class TestAudioProcessingService:
20
- """Test cases for AudioProcessingService."""
21
-
22
- @pytest.fixture
23
- def mock_stt_service(self):
24
- """Mock speech recognition service."""
25
- return Mock()
26
-
27
- @pytest.fixture
28
- def mock_translation_service(self):
29
- """Mock translation service."""
30
- return Mock()
31
-
32
- @pytest.fixture
33
- def mock_tts_service(self):
34
- """Mock speech synthesis service."""
35
- return Mock()
36
-
37
- @pytest.fixture
38
- def audio_processing_service(self, mock_stt_service, mock_translation_service, mock_tts_service):
39
- """AudioProcessingService instance with mocked dependencies."""
40
- return AudioProcessingService(
41
- speech_recognition_service=mock_stt_service,
42
- translation_service=mock_translation_service,
43
- speech_synthesis_service=mock_tts_service
44
- )
45
-
46
- @pytest.fixture
47
- def sample_audio(self):
48
- """Sample audio content for testing."""
49
- return AudioContent(
50
- data=b"fake_audio_data",
51
- format="wav",
52
- sample_rate=22050,
53
- duration=10.0,
54
- filename="test.wav"
55
- )
56
-
57
- @pytest.fixture
58
- def sample_voice_settings(self):
59
- """Sample voice settings for testing."""
60
- return VoiceSettings(
61
- voice_id="test_voice",
62
- speed=1.0,
63
- language="es"
64
- )
65
-
66
- @pytest.fixture
67
- def sample_text_content(self):
68
- """Sample text content for testing."""
69
- return TextContent(
70
- text="Hello world",
71
- language="en"
72
- )
73
-
74
- def test_successful_pipeline_processing(
75
- self,
76
- audio_processing_service,
77
- mock_stt_service,
78
- mock_translation_service,
79
- mock_tts_service,
80
- sample_audio,
81
- sample_voice_settings,
82
- sample_text_content
83
- ):
84
- """Test successful processing through the complete pipeline."""
85
- # Arrange
86
- original_text = TextContent(text="Hello world", language="en")
87
- translated_text = TextContent(text="Hola mundo", language="es")
88
- output_audio = AudioContent(
89
- data=b"synthesized_audio",
90
- format="wav",
91
- sample_rate=22050,
92
- duration=5.0
93
- )
94
-
95
- mock_stt_service.transcribe.return_value = original_text
96
- mock_translation_service.translate.return_value = translated_text
97
- mock_tts_service.synthesize.return_value = output_audio
98
-
99
- # Act
100
- result = audio_processing_service.process_audio_pipeline(
101
- audio=sample_audio,
102
- target_language="es",
103
- voice_settings=sample_voice_settings
104
- )
105
-
106
- # Assert
107
- assert result.success is True
108
- assert result.original_text == original_text
109
- assert result.translated_text == translated_text
110
- assert result.audio_output == output_audio
111
- assert result.error_message is None
112
- assert result.processing_time >= 0
113
-
114
- # Verify service calls
115
- mock_stt_service.transcribe.assert_called_once_with(sample_audio, "whisper-base")
116
- mock_translation_service.translate.assert_called_once()
117
- mock_tts_service.synthesize.assert_called_once()
118
-
119
- def test_no_translation_needed_same_language(
120
- self,
121
- audio_processing_service,
122
- mock_stt_service,
123
- mock_translation_service,
124
- mock_tts_service,
125
- sample_audio
126
- ):
127
- """Test pipeline when no translation is needed (same language)."""
128
- # Arrange
129
- original_text = TextContent(text="Hola mundo", language="es")
130
- voice_settings = VoiceSettings(voice_id="test_voice", speed=1.0, language="es")
131
- output_audio = AudioContent(
132
- data=b"synthesized_audio",
133
- format="wav",
134
- sample_rate=22050,
135
- duration=5.0
136
- )
137
-
138
- mock_stt_service.transcribe.return_value = original_text
139
- mock_tts_service.synthesize.return_value = output_audio
140
-
141
- # Act
142
- result = audio_processing_service.process_audio_pipeline(
143
- audio=sample_audio,
144
- target_language="es",
145
- voice_settings=voice_settings
146
- )
147
-
148
- # Assert
149
- assert result.success is True
150
- assert result.original_text == original_text
151
- assert result.translated_text == original_text # Same as original
152
- assert result.audio_output == output_audio
153
-
154
- # Translation service should not be called
155
- mock_translation_service.translate.assert_not_called()
156
-
157
- def test_validation_error_none_audio(self, audio_processing_service, sample_voice_settings):
158
- """Test validation error when audio is None."""
159
- # Act
160
- result = audio_processing_service.process_audio_pipeline(
161
- audio=None,
162
- target_language="es",
163
- voice_settings=sample_voice_settings
164
- )
165
-
166
- # Assert
167
- assert result.success is False
168
- assert "Audio content cannot be None" in result.error_message
169
-
170
- def test_validation_error_empty_target_language(self, audio_processing_service, sample_audio, sample_voice_settings):
171
- """Test validation error when target language is empty."""
172
- # Act
173
- result = audio_processing_service.process_audio_pipeline(
174
- audio=sample_audio,
175
- target_language="",
176
- voice_settings=sample_voice_settings
177
- )
178
-
179
- # Assert
180
- assert result.success is False
181
- assert "Target language cannot be empty" in result.error_message
182
-
183
- def test_validation_error_language_mismatch(self, audio_processing_service, sample_audio):
184
- """Test validation error when voice settings language doesn't match target language."""
185
- # Arrange
186
- voice_settings = VoiceSettings(voice_id="test_voice", speed=1.0, language="en")
187
-
188
- # Act
189
- result = audio_processing_service.process_audio_pipeline(
190
- audio=sample_audio,
191
- target_language="es",
192
- voice_settings=voice_settings
193
- )
194
-
195
- # Assert
196
- assert result.success is False
197
- assert "Voice settings language (en) must match target language (es)" in result.error_message
198
-
199
- def test_validation_error_audio_too_long(self, audio_processing_service, sample_voice_settings):
200
- """Test validation error when audio is too long."""
201
- # Arrange
202
- long_audio = AudioContent(
203
- data=b"fake_audio_data",
204
- format="wav",
205
- sample_rate=22050,
206
- duration=400.0 # Exceeds 300s limit
207
- )
208
-
209
- # Act
210
- result = audio_processing_service.process_audio_pipeline(
211
- audio=long_audio,
212
- target_language="es",
213
- voice_settings=sample_voice_settings
214
- )
215
-
216
- # Assert
217
- assert result.success is False
218
- assert "exceeds maximum allowed duration" in result.error_message
219
-
220
- def test_stt_failure_handling(
221
- self,
222
- audio_processing_service,
223
- mock_stt_service,
224
- sample_audio,
225
- sample_voice_settings
226
- ):
227
- """Test handling of STT service failure."""
228
- # Arrange
229
- mock_stt_service.transcribe.side_effect = Exception("STT service unavailable")
230
-
231
- # Act
232
- result = audio_processing_service.process_audio_pipeline(
233
- audio=sample_audio,
234
- target_language="es",
235
- voice_settings=sample_voice_settings
236
- )
237
-
238
- # Assert
239
- assert result.success is False
240
- assert "Speech recognition failed" in result.error_message
241
- assert result.processing_time >= 0
242
-
243
- def test_translation_failure_handling(
244
- self,
245
- audio_processing_service,
246
- mock_stt_service,
247
- mock_translation_service,
248
- sample_audio,
249
- sample_voice_settings
250
- ):
251
- """Test handling of translation service failure."""
252
- # Arrange
253
- original_text = TextContent(text="Hello world", language="en")
254
- mock_stt_service.transcribe.return_value = original_text
255
- mock_translation_service.translate.side_effect = Exception("Translation service unavailable")
256
-
257
- # Act
258
- result = audio_processing_service.process_audio_pipeline(
259
- audio=sample_audio,
260
- target_language="es",
261
- voice_settings=sample_voice_settings
262
- )
263
-
264
- # Assert
265
- assert result.success is False
266
- assert "Translation failed" in result.error_message
267
- assert result.processing_time >= 0
268
-
269
- def test_tts_failure_handling(
270
- self,
271
- audio_processing_service,
272
- mock_stt_service,
273
- mock_translation_service,
274
- mock_tts_service,
275
- sample_audio,
276
- sample_voice_settings
277
- ):
278
- """Test handling of TTS service failure."""
279
- # Arrange
280
- original_text = TextContent(text="Hello world", language="en")
281
- translated_text = TextContent(text="Hola mundo", language="es")
282
-
283
- mock_stt_service.transcribe.return_value = original_text
284
- mock_translation_service.translate.return_value = translated_text
285
- mock_tts_service.synthesize.side_effect = Exception("TTS service unavailable")
286
-
287
- # Act
288
- result = audio_processing_service.process_audio_pipeline(
289
- audio=sample_audio,
290
- target_language="es",
291
- voice_settings=sample_voice_settings
292
- )
293
-
294
- # Assert
295
- assert result.success is False
296
- assert "Speech synthesis failed" in result.error_message
297
- assert result.processing_time >= 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/domain/test_exceptions.py DELETED
@@ -1,240 +0,0 @@
1
- """Unit tests for domain exceptions."""
2
-
3
- import pytest
4
- from src.domain.exceptions import (
5
- DomainException,
6
- InvalidAudioFormatException,
7
- InvalidTextContentException,
8
- TranslationFailedException,
9
- SpeechRecognitionException,
10
- SpeechSynthesisException,
11
- InvalidVoiceSettingsException,
12
- AudioProcessingException
13
- )
14
-
15
-
16
- class TestDomainExceptions:
17
- """Test cases for domain exceptions."""
18
-
19
- def test_domain_exception_is_base_exception(self):
20
- """Test that DomainException is the base exception."""
21
- exception = DomainException("Base domain error")
22
-
23
- assert isinstance(exception, Exception)
24
- assert str(exception) == "Base domain error"
25
-
26
- def test_domain_exception_without_message(self):
27
- """Test DomainException without message."""
28
- exception = DomainException()
29
-
30
- assert isinstance(exception, Exception)
31
- assert str(exception) == ""
32
-
33
- def test_invalid_audio_format_exception_inheritance(self):
34
- """Test that InvalidAudioFormatException inherits from DomainException."""
35
- exception = InvalidAudioFormatException("Invalid audio format")
36
-
37
- assert isinstance(exception, DomainException)
38
- assert isinstance(exception, Exception)
39
- assert str(exception) == "Invalid audio format"
40
-
41
- def test_invalid_audio_format_exception_usage(self):
42
- """Test InvalidAudioFormatException usage scenario."""
43
- try:
44
- raise InvalidAudioFormatException("Unsupported format: xyz")
45
- except DomainException as e:
46
- assert "Unsupported format: xyz" in str(e)
47
- except Exception:
48
- pytest.fail("Should have caught as DomainException")
49
-
50
- def test_invalid_text_content_exception_inheritance(self):
51
- """Test that InvalidTextContentException inherits from DomainException."""
52
- exception = InvalidTextContentException("Invalid text content")
53
-
54
- assert isinstance(exception, DomainException)
55
- assert isinstance(exception, Exception)
56
- assert str(exception) == "Invalid text content"
57
-
58
- def test_invalid_text_content_exception_usage(self):
59
- """Test InvalidTextContentException usage scenario."""
60
- try:
61
- raise InvalidTextContentException("Text content is empty")
62
- except DomainException as e:
63
- assert "Text content is empty" in str(e)
64
- except Exception:
65
- pytest.fail("Should have caught as DomainException")
66
-
67
- def test_translation_failed_exception_inheritance(self):
68
- """Test that TranslationFailedException inherits from DomainException."""
69
- exception = TranslationFailedException("Translation failed")
70
-
71
- assert isinstance(exception, DomainException)
72
- assert isinstance(exception, Exception)
73
- assert str(exception) == "Translation failed"
74
-
75
- def test_translation_failed_exception_usage(self):
76
- """Test TranslationFailedException usage scenario."""
77
- try:
78
- raise TranslationFailedException("Translation service unavailable")
79
- except DomainException as e:
80
- assert "Translation service unavailable" in str(e)
81
- except Exception:
82
- pytest.fail("Should have caught as DomainException")
83
-
84
- def test_speech_recognition_exception_inheritance(self):
85
- """Test that SpeechRecognitionException inherits from DomainException."""
86
- exception = SpeechRecognitionException("Speech recognition failed")
87
-
88
- assert isinstance(exception, DomainException)
89
- assert isinstance(exception, Exception)
90
- assert str(exception) == "Speech recognition failed"
91
-
92
- def test_speech_recognition_exception_usage(self):
93
- """Test SpeechRecognitionException usage scenario."""
94
- try:
95
- raise SpeechRecognitionException("STT model not available")
96
- except DomainException as e:
97
- assert "STT model not available" in str(e)
98
- except Exception:
99
- pytest.fail("Should have caught as DomainException")
100
-
101
- def test_speech_synthesis_exception_inheritance(self):
102
- """Test that SpeechSynthesisException inherits from DomainException."""
103
- exception = SpeechSynthesisException("Speech synthesis failed")
104
-
105
- assert isinstance(exception, DomainException)
106
- assert isinstance(exception, Exception)
107
- assert str(exception) == "Speech synthesis failed"
108
-
109
- def test_speech_synthesis_exception_usage(self):
110
- """Test SpeechSynthesisException usage scenario."""
111
- try:
112
- raise SpeechSynthesisException("TTS voice not found")
113
- except DomainException as e:
114
- assert "TTS voice not found" in str(e)
115
- except Exception:
116
- pytest.fail("Should have caught as DomainException")
117
-
118
- def test_invalid_voice_settings_exception_inheritance(self):
119
- """Test that InvalidVoiceSettingsException inherits from DomainException."""
120
- exception = InvalidVoiceSettingsException("Invalid voice settings")
121
-
122
- assert isinstance(exception, DomainException)
123
- assert isinstance(exception, Exception)
124
- assert str(exception) == "Invalid voice settings"
125
-
126
- def test_invalid_voice_settings_exception_usage(self):
127
- """Test InvalidVoiceSettingsException usage scenario."""
128
- try:
129
- raise InvalidVoiceSettingsException("Voice speed out of range")
130
- except DomainException as e:
131
- assert "Voice speed out of range" in str(e)
132
- except Exception:
133
- pytest.fail("Should have caught as DomainException")
134
-
135
- def test_audio_processing_exception_inheritance(self):
136
- """Test that AudioProcessingException inherits from DomainException."""
137
- exception = AudioProcessingException("Audio processing failed")
138
-
139
- assert isinstance(exception, DomainException)
140
- assert isinstance(exception, Exception)
141
- assert str(exception) == "Audio processing failed"
142
-
143
- def test_audio_processing_exception_usage(self):
144
- """Test AudioProcessingException usage scenario."""
145
- try:
146
- raise AudioProcessingException("Pipeline validation failed")
147
- except DomainException as e:
148
- assert "Pipeline validation failed" in str(e)
149
- except Exception:
150
- pytest.fail("Should have caught as DomainException")
151
-
152
- def test_all_exceptions_inherit_from_domain_exception(self):
153
- """Test that all domain exceptions inherit from DomainException."""
154
- exceptions = [
155
- InvalidAudioFormatException("test"),
156
- InvalidTextContentException("test"),
157
- TranslationFailedException("test"),
158
- SpeechRecognitionException("test"),
159
- SpeechSynthesisException("test"),
160
- InvalidVoiceSettingsException("test"),
161
- AudioProcessingException("test")
162
- ]
163
-
164
- for exception in exceptions:
165
- assert isinstance(exception, DomainException)
166
- assert isinstance(exception, Exception)
167
-
168
- def test_exception_chaining_support(self):
169
- """Test that exceptions support chaining."""
170
- original_error = ValueError("Original error")
171
-
172
- try:
173
- raise TranslationFailedException("Translation failed") from original_error
174
- except TranslationFailedException as e:
175
- assert e.__cause__ is original_error
176
- assert str(e) == "Translation failed"
177
-
178
- def test_exception_with_none_message(self):
179
- """Test exceptions with None message."""
180
- exception = AudioProcessingException(None)
181
-
182
- assert isinstance(exception, DomainException)
183
- # Python converts None to empty string for exception messages
184
- assert str(exception) == "None"
185
-
186
- def test_exception_hierarchy_catching(self):
187
- """Test catching exceptions at different levels of hierarchy."""
188
- # Test catching specific exception
189
- try:
190
- raise SpeechSynthesisException("TTS failed")
191
- except SpeechSynthesisException as e:
192
- assert "TTS failed" in str(e)
193
- except Exception:
194
- pytest.fail("Should have caught SpeechSynthesisException")
195
-
196
- # Test catching at domain level
197
- try:
198
- raise SpeechSynthesisException("TTS failed")
199
- except DomainException as e:
200
- assert "TTS failed" in str(e)
201
- except Exception:
202
- pytest.fail("Should have caught as DomainException")
203
-
204
- # Test catching at base level
205
- try:
206
- raise SpeechSynthesisException("TTS failed")
207
- except Exception as e:
208
- assert "TTS failed" in str(e)
209
-
210
- def test_exception_equality(self):
211
- """Test exception equality comparison."""
212
- exc1 = AudioProcessingException("Same message")
213
- exc2 = AudioProcessingException("Same message")
214
- exc3 = AudioProcessingException("Different message")
215
-
216
- # Exceptions are not equal even with same message (different instances)
217
- assert exc1 is not exc2
218
- assert exc1 is not exc3
219
-
220
- # But they have the same type and message
221
- assert type(exc1) == type(exc2)
222
- assert str(exc1) == str(exc2)
223
- assert str(exc1) != str(exc3)
224
-
225
- def test_exception_repr(self):
226
- """Test exception string representation."""
227
- exception = TranslationFailedException("Translation service error")
228
-
229
- # Test that repr includes class name and message
230
- repr_str = repr(exception)
231
- assert "TranslationFailedException" in repr_str
232
- assert "Translation service error" in repr_str
233
-
234
- def test_exception_args_property(self):
235
- """Test exception args property."""
236
- message = "Test error message"
237
- exception = SpeechRecognitionException(message)
238
-
239
- assert exception.args == (message,)
240
- assert exception.args[0] == message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/infrastructure/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Infrastructure layer unit tests."""
 
 
tests/unit/infrastructure/base/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Base provider unit tests."""
 
 
tests/unit/infrastructure/base/test_stt_provider_base.py DELETED
@@ -1,359 +0,0 @@
1
- """Unit tests for STTProviderBase abstract class."""
2
-
3
- import pytest
4
- from unittest.mock import Mock, patch, MagicMock
5
- import tempfile
6
- from pathlib import Path
7
-
8
- from src.infrastructure.base.stt_provider_base import STTProviderBase
9
- from src.domain.models.audio_content import AudioContent
10
- from src.domain.models.text_content import TextContent
11
- from src.domain.exceptions import SpeechRecognitionException
12
-
13
-
14
- class ConcreteSTTProvider(STTProviderBase):
15
- """Concrete implementation for testing."""
16
-
17
- def __init__(self, provider_name="test", supported_languages=None, available=True, models=None):
18
- super().__init__(provider_name, supported_languages)
19
- self._available = available
20
- self._models = models or ["model1", "model2"]
21
- self._should_fail = False
22
- self._transcription_result = "Hello world"
23
-
24
- def _perform_transcription(self, audio_path, model):
25
- if self._should_fail:
26
- raise Exception("Test transcription error")
27
- return self._transcription_result
28
-
29
- def is_available(self):
30
- return self._available
31
-
32
- def get_available_models(self):
33
- return self._models
34
-
35
- def get_default_model(self):
36
- return self._models[0] if self._models else "default"
37
-
38
- def set_should_fail(self, should_fail):
39
- self._should_fail = should_fail
40
-
41
- def set_transcription_result(self, result):
42
- self._transcription_result = result
43
-
44
-
45
- class TestSTTProviderBase:
46
- """Test cases for STTProviderBase abstract class."""
47
-
48
- def setup_method(self):
49
- """Set up test fixtures."""
50
- self.provider = ConcreteSTTProvider()
51
- self.audio_content = AudioContent(
52
- data=b"fake_audio_data",
53
- format="wav",
54
- sample_rate=16000,
55
- duration=5.0,
56
- filename="test.wav"
57
- )
58
-
59
- def test_provider_initialization(self):
60
- """Test provider initialization with default values."""
61
- provider = ConcreteSTTProvider("test_provider", ["en", "es"])
62
-
63
- assert provider.provider_name == "test_provider"
64
- assert provider.supported_languages == ["en", "es"]
65
- assert isinstance(provider._temp_dir, Path)
66
- assert provider._temp_dir.exists()
67
-
68
- def test_provider_initialization_no_languages(self):
69
- """Test provider initialization without supported languages."""
70
- provider = ConcreteSTTProvider("test_provider")
71
-
72
- assert provider.provider_name == "test_provider"
73
- assert provider.supported_languages == []
74
-
75
- @patch('builtins.open', create=True)
76
- def test_transcribe_success(self, mock_open):
77
- """Test successful transcription."""
78
- mock_file = MagicMock()
79
- mock_open.return_value.__enter__.return_value = mock_file
80
-
81
- result = self.provider.transcribe(self.audio_content, "model1")
82
-
83
- assert isinstance(result, TextContent)
84
- assert result.text == "Hello world"
85
- assert result.language == "en"
86
- assert result.encoding == "utf-8"
87
-
88
- def test_transcribe_empty_audio_fails(self):
89
- """Test that empty audio data raises exception."""
90
- empty_audio = AudioContent(
91
- data=b"",
92
- format="wav",
93
- sample_rate=16000,
94
- duration=0.1
95
- )
96
-
97
- with pytest.raises(SpeechRecognitionException, match="Audio data cannot be empty"):
98
- self.provider.transcribe(empty_audio, "model1")
99
-
100
- def test_transcribe_audio_too_long_fails(self):
101
- """Test that audio longer than 1 hour raises exception."""
102
- long_audio = AudioContent(
103
- data=b"fake_audio_data",
104
- format="wav",
105
- sample_rate=16000,
106
- duration=3601.0 # Over 1 hour
107
- )
108
-
109
- with pytest.raises(SpeechRecognitionException, match="Audio duration exceeds maximum limit"):
110
- self.provider.transcribe(long_audio, "model1")
111
-
112
- def test_transcribe_audio_too_short_fails(self):
113
- """Test that audio shorter than 100ms raises exception."""
114
- short_audio = AudioContent(
115
- data=b"fake_audio_data",
116
- format="wav",
117
- sample_rate=16000,
118
- duration=0.05 # 50ms
119
- )
120
-
121
- with pytest.raises(SpeechRecognitionException, match="Audio duration too short"):
122
- self.provider.transcribe(short_audio, "model1")
123
-
124
- def test_transcribe_invalid_format_fails(self):
125
- """Test that invalid audio format raises exception."""
126
- # Create audio with invalid format by mocking is_valid_format
127
- invalid_audio = AudioContent(
128
- data=b"fake_audio_data",
129
- format="wav",
130
- sample_rate=16000,
131
- duration=5.0
132
- )
133
-
134
- with patch.object(invalid_audio, 'is_valid_format', False):
135
- with pytest.raises(SpeechRecognitionException, match="Unsupported audio format"):
136
- self.provider.transcribe(invalid_audio, "model1")
137
-
138
- @patch('builtins.open', create=True)
139
- def test_transcribe_provider_error(self, mock_open):
140
- """Test handling of provider-specific errors."""
141
- mock_file = MagicMock()
142
- mock_open.return_value.__enter__.return_value = mock_file
143
-
144
- self.provider.set_should_fail(True)
145
-
146
- with pytest.raises(SpeechRecognitionException, match="STT transcription failed"):
147
- self.provider.transcribe(self.audio_content, "model1")
148
-
149
- @patch('builtins.open', create=True)
150
- @patch('pathlib.Path.unlink')
151
- def test_transcribe_cleanup_temp_file(self, mock_unlink, mock_open):
152
- """Test that temporary files are cleaned up."""
153
- mock_file = MagicMock()
154
- mock_open.return_value.__enter__.return_value = mock_file
155
-
156
- self.provider.transcribe(self.audio_content, "model1")
157
-
158
- # Verify cleanup was attempted
159
- mock_unlink.assert_called()
160
-
161
- @patch('builtins.open', create=True)
162
- def test_preprocess_audio(self, mock_open):
163
- """Test audio preprocessing."""
164
- mock_file = MagicMock()
165
- mock_open.return_value.__enter__.return_value = mock_file
166
-
167
- processed_path = self.provider._preprocess_audio(self.audio_content)
168
-
169
- assert isinstance(processed_path, Path)
170
- assert processed_path.suffix == ".wav"
171
- mock_file.write.assert_called_once_with(self.audio_content.data)
172
-
173
- def test_preprocess_audio_error(self):
174
- """Test audio preprocessing error handling."""
175
- with patch('builtins.open', side_effect=IOError("Test error")):
176
- with pytest.raises(SpeechRecognitionException, match="Audio preprocessing failed"):
177
- self.provider._preprocess_audio(self.audio_content)
178
-
179
- @patch('pydub.AudioSegment.from_wav')
180
- @patch('pydub.AudioSegment.export')
181
- def test_convert_audio_format_wav(self, mock_export, mock_from_wav):
182
- """Test audio format conversion for WAV."""
183
- mock_audio = Mock()
184
- mock_audio.set_frame_rate.return_value.set_channels.return_value = mock_audio
185
- mock_from_wav.return_value = mock_audio
186
-
187
- test_path = Path("/tmp/test.wav")
188
- result_path = self.provider._convert_audio_format(test_path, self.audio_content)
189
-
190
- mock_from_wav.assert_called_once_with(test_path)
191
- mock_audio.set_frame_rate.assert_called_once_with(16000)
192
- mock_audio.set_channels.assert_called_once_with(1)
193
- mock_export.assert_called_once()
194
-
195
- @patch('pydub.AudioSegment.from_mp3')
196
- def test_convert_audio_format_mp3(self, mock_from_mp3):
197
- """Test audio format conversion for MP3."""
198
- mp3_audio = AudioContent(
199
- data=b"fake_mp3_data",
200
- format="mp3",
201
- sample_rate=44100,
202
- duration=5.0
203
- )
204
-
205
- mock_audio = Mock()
206
- mock_audio.set_frame_rate.return_value.set_channels.return_value = mock_audio
207
- mock_from_mp3.return_value = mock_audio
208
-
209
- test_path = Path("/tmp/test.mp3")
210
- self.provider._convert_audio_format(test_path, mp3_audio)
211
-
212
- mock_from_mp3.assert_called_once_with(test_path)
213
-
214
- def test_convert_audio_format_no_pydub(self):
215
- """Test audio format conversion when pydub is not available."""
216
- test_path = Path("/tmp/test.wav")
217
-
218
- with patch('pydub.AudioSegment', side_effect=ImportError("pydub not available")):
219
- result_path = self.provider._convert_audio_format(test_path, self.audio_content)
220
-
221
- # Should return original path when pydub is not available
222
- assert result_path == test_path
223
-
224
- def test_convert_audio_format_error(self):
225
- """Test audio format conversion error handling."""
226
- test_path = Path("/tmp/test.wav")
227
-
228
- with patch('pydub.AudioSegment.from_wav', side_effect=Exception("Conversion error")):
229
- result_path = self.provider._convert_audio_format(test_path, self.audio_content)
230
-
231
- # Should return original path on error
232
- assert result_path == test_path
233
-
234
- def test_detect_language_english(self):
235
- """Test language detection for English text."""
236
- english_text = "The quick brown fox jumps over the lazy dog and it is very nice"
237
- language = self.provider._detect_language(english_text)
238
- assert language == "en"
239
-
240
- def test_detect_language_few_indicators(self):
241
- """Test language detection with few English indicators."""
242
- text = "Hello world"
243
- language = self.provider._detect_language(text)
244
- assert language == "en"
245
-
246
- def test_detect_language_no_indicators(self):
247
- """Test language detection with no clear indicators."""
248
- text = "xyz abc def"
249
- language = self.provider._detect_language(text)
250
- assert language == "en" # Should default to English
251
-
252
- def test_detect_language_error(self):
253
- """Test language detection error handling."""
254
- with patch.object(self.provider, '_detect_language', side_effect=Exception("Detection error")):
255
- language = self.provider._detect_language("test")
256
- assert language is None
257
-
258
- def test_ensure_temp_directory(self):
259
- """Test temporary directory creation."""
260
- temp_dir = self.provider._ensure_temp_directory()
261
-
262
- assert isinstance(temp_dir, Path)
263
- assert temp_dir.exists()
264
- assert temp_dir.is_dir()
265
- assert "stt_temp" in str(temp_dir)
266
-
267
- def test_cleanup_temp_file(self):
268
- """Test temporary file cleanup."""
269
- # Create a temporary file
270
- temp_file = self.provider._temp_dir / "test_file.wav"
271
- temp_file.touch()
272
-
273
- assert temp_file.exists()
274
-
275
- self.provider._cleanup_temp_file(temp_file)
276
-
277
- assert not temp_file.exists()
278
-
279
- def test_cleanup_temp_file_not_exists(self):
280
- """Test cleanup of non-existent file."""
281
- non_existent = Path("/tmp/non_existent_file.wav")
282
-
283
- # Should not raise exception
284
- self.provider._cleanup_temp_file(non_existent)
285
-
286
- def test_cleanup_temp_file_error(self):
287
- """Test cleanup error handling."""
288
- with patch('pathlib.Path.unlink', side_effect=OSError("Permission denied")):
289
- temp_file = Path("/tmp/test.wav")
290
-
291
- # Should not raise exception
292
- self.provider._cleanup_temp_file(temp_file)
293
-
294
- @patch('time.time')
295
- @patch('pathlib.Path.glob')
296
- def test_cleanup_old_temp_files(self, mock_glob, mock_time):
297
- """Test cleanup of old temporary files."""
298
- mock_time.return_value = 1000000
299
-
300
- # Mock old file
301
- old_file = Mock()
302
- old_file.is_file.return_value = True
303
- old_file.stat.return_value.st_mtime = 900000 # Old file
304
-
305
- # Mock recent file
306
- recent_file = Mock()
307
- recent_file.is_file.return_value = True
308
- recent_file.stat.return_value.st_mtime = 999000 # Recent file
309
-
310
- mock_glob.return_value = [old_file, recent_file]
311
-
312
- self.provider._cleanup_old_temp_files(24)
313
-
314
- # Old file should be deleted
315
- old_file.unlink.assert_called_once()
316
- recent_file.unlink.assert_not_called()
317
-
318
- def test_cleanup_old_temp_files_error(self):
319
- """Test cleanup error handling."""
320
- with patch.object(self.provider._temp_dir, 'glob', side_effect=Exception("Test error")):
321
- # Should not raise exception
322
- self.provider._cleanup_old_temp_files()
323
-
324
- def test_handle_provider_error(self):
325
- """Test provider error handling."""
326
- original_error = ValueError("Original error")
327
-
328
- with pytest.raises(SpeechRecognitionException) as exc_info:
329
- self.provider._handle_provider_error(original_error, "testing")
330
-
331
- assert "test error during testing: Original error" in str(exc_info.value)
332
- assert exc_info.value.__cause__ is original_error
333
-
334
- def test_handle_provider_error_no_context(self):
335
- """Test provider error handling without context."""
336
- original_error = ValueError("Original error")
337
-
338
- with pytest.raises(SpeechRecognitionException) as exc_info:
339
- self.provider._handle_provider_error(original_error)
340
-
341
- assert "test error: Original error" in str(exc_info.value)
342
- assert exc_info.value.__cause__ is original_error
343
-
344
- def test_abstract_methods_not_implemented(self):
345
- """Test that abstract methods raise NotImplementedError."""
346
- # Create instance of base class directly (should fail)
347
- with pytest.raises(TypeError):
348
- STTProviderBase("test")
349
-
350
- def test_provider_unavailable(self):
351
- """Test behavior when provider is unavailable."""
352
- provider = ConcreteSTTProvider(available=False)
353
- assert provider.is_available() is False
354
-
355
- def test_no_models_available(self):
356
- """Test behavior when no models are available."""
357
- provider = ConcreteSTTProvider(models=[])
358
- assert provider.get_available_models() == []
359
- assert provider.get_default_model() == "default"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/infrastructure/base/test_translation_provider_base.py DELETED
@@ -1,325 +0,0 @@
1
- """Unit tests for TranslationProviderBase abstract class."""
2
-
3
- import pytest
4
- from unittest.mock import Mock, patch
5
-
6
- from src.infrastructure.base.translation_provider_base import TranslationProviderBase
7
- from src.domain.models.translation_request import TranslationRequest
8
- from src.domain.models.text_content import TextContent
9
- from src.domain.exceptions import TranslationFailedException
10
-
11
-
12
- class ConcreteTranslationProvider(TranslationProviderBase):
13
- """Concrete implementation for testing."""
14
-
15
- def __init__(self, provider_name="test", supported_languages=None, available=True):
16
- super().__init__(provider_name, supported_languages)
17
- self._available = available
18
- self._should_fail = False
19
- self._translation_result = "Translated text"
20
-
21
- def _translate_chunk(self, text, source_language, target_language):
22
- if self._should_fail:
23
- raise Exception("Test translation error")
24
- return f"{self._translation_result} ({source_language}->{target_language})"
25
-
26
- def is_available(self):
27
- return self._available
28
-
29
- def get_supported_languages(self):
30
- return self.supported_languages
31
-
32
- def set_should_fail(self, should_fail):
33
- self._should_fail = should_fail
34
-
35
- def set_translation_result(self, result):
36
- self._translation_result = result
37
-
38
-
39
- class TestTranslationProviderBase:
40
- """Test cases for TranslationProviderBase abstract class."""
41
-
42
- def setup_method(self):
43
- """Set up test fixtures."""
44
- self.provider = ConcreteTranslationProvider()
45
- self.source_text = TextContent(text="Hello world", language="en")
46
- self.request = TranslationRequest(
47
- source_text=self.source_text,
48
- target_language="es"
49
- )
50
-
51
- def test_provider_initialization(self):
52
- """Test provider initialization with default values."""
53
- supported_langs = {"en": ["es", "fr"], "es": ["en"]}
54
- provider = ConcreteTranslationProvider("test_provider", supported_langs)
55
-
56
- assert provider.provider_name == "test_provider"
57
- assert provider.supported_languages == supported_langs
58
- assert provider.max_chunk_length == 1000
59
-
60
- def test_provider_initialization_no_languages(self):
61
- """Test provider initialization without supported languages."""
62
- provider = ConcreteTranslationProvider("test_provider")
63
-
64
- assert provider.provider_name == "test_provider"
65
- assert provider.supported_languages == {}
66
-
67
- def test_translate_success(self):
68
- """Test successful translation."""
69
- result = self.provider.translate(self.request)
70
-
71
- assert isinstance(result, TextContent)
72
- assert result.text == "Translated text (en->es)"
73
- assert result.language == "es"
74
- assert result.encoding == "utf-8"
75
-
76
- def test_translate_with_language_validation(self):
77
- """Test translation with language validation."""
78
- supported_langs = {"en": ["es", "fr"], "es": ["en"]}
79
- provider = ConcreteTranslationProvider("test", supported_langs)
80
-
81
- # Valid language pair should work
82
- result = provider.translate(self.request)
83
- assert isinstance(result, TextContent)
84
-
85
- # Invalid source language should fail
86
- invalid_request = TranslationRequest(
87
- source_text=TextContent(text="Hello", language="de"),
88
- target_language="es"
89
- )
90
-
91
- with pytest.raises(TranslationFailedException, match="Source language de not supported"):
92
- provider.translate(invalid_request)
93
-
94
- # Invalid target language should fail
95
- invalid_request2 = TranslationRequest(
96
- source_text=self.source_text,
97
- target_language="de"
98
- )
99
-
100
- with pytest.raises(TranslationFailedException, match="Translation from en to de not supported"):
101
- provider.translate(invalid_request2)
102
-
103
- def test_translate_empty_text_fails(self):
104
- """Test that empty text raises exception."""
105
- empty_request = TranslationRequest(
106
- source_text=TextContent(text="", language="en"),
107
- target_language="es"
108
- )
109
-
110
- with pytest.raises(TranslationFailedException, match="Source text cannot be empty"):
111
- self.provider.translate(empty_request)
112
-
113
- def test_translate_whitespace_text_fails(self):
114
- """Test that whitespace-only text raises exception."""
115
- whitespace_request = TranslationRequest(
116
- source_text=TextContent(text=" ", language="en"),
117
- target_language="es"
118
- )
119
-
120
- with pytest.raises(TranslationFailedException, match="Source text cannot be empty"):
121
- self.provider.translate(whitespace_request)
122
-
123
- def test_translate_same_language_fails(self):
124
- """Test that same source and target language raises exception."""
125
- same_lang_request = TranslationRequest(
126
- source_text=self.source_text,
127
- target_language="en"
128
- )
129
-
130
- with pytest.raises(TranslationFailedException, match="Source and target languages cannot be the same"):
131
- self.provider.translate(same_lang_request)
132
-
133
- def test_translate_provider_error(self):
134
- """Test handling of provider-specific errors."""
135
- self.provider.set_should_fail(True)
136
-
137
- with pytest.raises(TranslationFailedException, match="Translation failed"):
138
- self.provider.translate(self.request)
139
-
140
- def test_translate_long_text_chunking(self):
141
- """Test translation of long text with chunking."""
142
- # Create long text that will be chunked
143
- long_text = "This is a sentence. " * 100 # Much longer than default chunk size
144
- long_request = TranslationRequest(
145
- source_text=TextContent(text=long_text, language="en"),
146
- target_language="es"
147
- )
148
-
149
- result = self.provider.translate(long_request)
150
-
151
- assert isinstance(result, TextContent)
152
- # Should contain multiple translated chunks
153
- assert "Translated text (en->es)" in result.text
154
-
155
- def test_chunk_text_short_text(self):
156
- """Test text chunking with short text."""
157
- short_text = "Hello world"
158
- chunks = self.provider._chunk_text(short_text)
159
-
160
- assert len(chunks) == 1
161
- assert chunks[0] == short_text
162
-
163
- def test_chunk_text_long_text(self):
164
- """Test text chunking with long text."""
165
- # Create text longer than chunk size
166
- long_text = "This is a sentence. " * 100
167
- self.provider.max_chunk_length = 50 # Small chunk size for testing
168
-
169
- chunks = self.provider._chunk_text(long_text)
170
-
171
- assert len(chunks) > 1
172
- for chunk in chunks:
173
- assert len(chunk) <= self.provider.max_chunk_length
174
-
175
- def test_split_into_sentences(self):
176
- """Test sentence splitting."""
177
- text = "First sentence. Second sentence! Third sentence? Fourth sentence."
178
- sentences = self.provider._split_into_sentences(text)
179
-
180
- assert len(sentences) == 4
181
- assert "First sentence" in sentences[0]
182
- assert "Second sentence" in sentences[1]
183
- assert "Third sentence" in sentences[2]
184
- assert "Fourth sentence" in sentences[3]
185
-
186
- def test_split_into_sentences_no_punctuation(self):
187
- """Test sentence splitting with no punctuation."""
188
- text = "Just one long sentence without proper punctuation"
189
- sentences = self.provider._split_into_sentences(text)
190
-
191
- assert len(sentences) == 1
192
- assert sentences[0] == text
193
-
194
- def test_split_long_sentence(self):
195
- """Test splitting of long sentences by words."""
196
- long_sentence = "word " * 100 # Very long sentence
197
- self.provider.max_chunk_length = 20 # Small chunk size
198
-
199
- chunks = self.provider._split_long_sentence(long_sentence)
200
-
201
- assert len(chunks) > 1
202
- for chunk in chunks:
203
- assert len(chunk) <= self.provider.max_chunk_length
204
-
205
- def test_split_long_sentence_single_long_word(self):
206
- """Test splitting with a single very long word."""
207
- long_word = "a" * 100
208
- self.provider.max_chunk_length = 20
209
-
210
- chunks = self.provider._split_long_sentence(long_word)
211
-
212
- assert len(chunks) == 1
213
- assert chunks[0] == long_word # Should include the long word as-is
214
-
215
- def test_reassemble_chunks(self):
216
- """Test reassembling translated chunks."""
217
- chunks = ["First chunk", "Second chunk", "Third chunk"]
218
- result = self.provider._reassemble_chunks(chunks)
219
-
220
- assert result == "First chunk Second chunk Third chunk"
221
-
222
- def test_reassemble_chunks_with_empty(self):
223
- """Test reassembling chunks with empty strings."""
224
- chunks = ["First chunk", "", "Third chunk", " "]
225
- result = self.provider._reassemble_chunks(chunks)
226
-
227
- assert result == "First chunk Third chunk"
228
-
229
- def test_preprocess_text(self):
230
- """Test text preprocessing."""
231
- messy_text = " Hello world \n\n with extra spaces "
232
- processed = self.provider._preprocess_text(messy_text)
233
-
234
- assert processed == "Hello world with extra spaces"
235
-
236
- def test_postprocess_text(self):
237
- """Test text postprocessing."""
238
- messy_text = "Hello world . This is a test ! Another sentence ?"
239
- processed = self.provider._postprocess_text(messy_text)
240
-
241
- assert processed == "Hello world. This is a test! Another sentence?"
242
-
243
- def test_postprocess_text_sentence_spacing(self):
244
- """Test postprocessing fixes sentence spacing."""
245
- text = "First sentence.Second sentence!Third sentence?"
246
- processed = self.provider._postprocess_text(text)
247
-
248
- assert processed == "First sentence. Second sentence! Third sentence?"
249
-
250
- def test_handle_provider_error(self):
251
- """Test provider error handling."""
252
- original_error = ValueError("Original error")
253
-
254
- with pytest.raises(TranslationFailedException) as exc_info:
255
- self.provider._handle_provider_error(original_error, "testing")
256
-
257
- assert "test error during testing: Original error" in str(exc_info.value)
258
- assert exc_info.value.__cause__ is original_error
259
-
260
- def test_handle_provider_error_no_context(self):
261
- """Test provider error handling without context."""
262
- original_error = ValueError("Original error")
263
-
264
- with pytest.raises(TranslationFailedException) as exc_info:
265
- self.provider._handle_provider_error(original_error)
266
-
267
- assert "test error: Original error" in str(exc_info.value)
268
- assert exc_info.value.__cause__ is original_error
269
-
270
- def test_set_chunk_size(self):
271
- """Test setting chunk size."""
272
- self.provider.set_chunk_size(500)
273
- assert self.provider.max_chunk_length == 500
274
-
275
- def test_set_chunk_size_invalid(self):
276
- """Test setting invalid chunk size."""
277
- with pytest.raises(ValueError, match="Chunk size must be positive"):
278
- self.provider.set_chunk_size(0)
279
-
280
- with pytest.raises(ValueError, match="Chunk size must be positive"):
281
- self.provider.set_chunk_size(-1)
282
-
283
- def test_get_translation_stats(self):
284
- """Test getting translation statistics."""
285
- stats = self.provider.get_translation_stats(self.request)
286
-
287
- assert stats['provider'] == 'test'
288
- assert stats['source_language'] == 'en'
289
- assert stats['target_language'] == 'es'
290
- assert stats['text_length'] == len(self.request.source_text.text)
291
- assert stats['word_count'] == len(self.request.source_text.text.split())
292
- assert stats['chunk_count'] >= 1
293
- assert 'max_chunk_length' in stats
294
- assert 'avg_chunk_length' in stats
295
-
296
- def test_get_translation_stats_empty_text(self):
297
- """Test getting translation statistics for empty text."""
298
- empty_request = TranslationRequest(
299
- source_text=TextContent(text="", language="en"),
300
- target_language="es"
301
- )
302
-
303
- stats = self.provider.get_translation_stats(empty_request)
304
-
305
- assert stats['text_length'] == 0
306
- assert stats['word_count'] == 0
307
- assert stats['chunk_count'] == 0
308
- assert stats['max_chunk_length'] == 0
309
- assert stats['avg_chunk_length'] == 0
310
-
311
- def test_abstract_methods_not_implemented(self):
312
- """Test that abstract methods raise NotImplementedError."""
313
- # Create instance of base class directly (should fail)
314
- with pytest.raises(TypeError):
315
- TranslationProviderBase("test")
316
-
317
- def test_provider_unavailable(self):
318
- """Test behavior when provider is unavailable."""
319
- provider = ConcreteTranslationProvider(available=False)
320
- assert provider.is_available() is False
321
-
322
- def test_no_supported_languages(self):
323
- """Test behavior when no languages are supported."""
324
- provider = ConcreteTranslationProvider(supported_languages={})
325
- assert provider.get_supported_languages() == {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/infrastructure/base/test_tts_provider_base.py DELETED
@@ -1,297 +0,0 @@
1
- """Unit tests for TTSProviderBase abstract class."""
2
-
3
- import pytest
4
- from unittest.mock import Mock, patch, MagicMock
5
- import tempfile
6
- from pathlib import Path
7
- import time
8
-
9
- from src.infrastructure.base.tts_provider_base import TTSProviderBase
10
- from src.domain.models.speech_synthesis_request import SpeechSynthesisRequest
11
- from src.domain.models.text_content import TextContent
12
- from src.domain.models.voice_settings import VoiceSettings
13
- from src.domain.models.audio_content import AudioContent
14
- from src.domain.models.audio_chunk import AudioChunk
15
- from src.domain.exceptions import SpeechSynthesisException
16
-
17
-
18
- class ConcreteTTSProvider(TTSProviderBase):
19
- """Concrete implementation for testing."""
20
-
21
- def __init__(self, provider_name="test", supported_languages=None, available=True, voices=None):
22
- super().__init__(provider_name, supported_languages)
23
- self._available = available
24
- self._voices = voices or ["voice1", "voice2"]
25
- self._should_fail = False
26
-
27
- def _generate_audio(self, request):
28
- if self._should_fail:
29
- raise Exception("Test error")
30
- return b"fake_audio_data", 44100
31
-
32
- def _generate_audio_stream(self, request):
33
- if self._should_fail:
34
- raise Exception("Test stream error")
35
- chunks = [
36
- (b"chunk1", 44100, False),
37
- (b"chunk2", 44100, False),
38
- (b"chunk3", 44100, True)
39
- ]
40
- for chunk in chunks:
41
- yield chunk
42
-
43
- def is_available(self):
44
- return self._available
45
-
46
- def get_available_voices(self):
47
- return self._voices
48
-
49
- def set_should_fail(self, should_fail):
50
- self._should_fail = should_fail
51
-
52
-
53
- class TestTTSProviderBase:
54
- """Test cases for TTSProviderBase abstract class."""
55
-
56
- def setup_method(self):
57
- """Set up test fixtures."""
58
- self.provider = ConcreteTTSProvider()
59
- self.text_content = TextContent(text="Hello world", language="en")
60
- self.voice_settings = VoiceSettings(voice_id="voice1", speed=1.0, pitch=1.0)
61
- self.request = SpeechSynthesisRequest(
62
- text_content=self.text_content,
63
- voice_settings=self.voice_settings
64
- )
65
-
66
- def test_provider_initialization(self):
67
- """Test provider initialization with default values."""
68
- provider = ConcreteTTSProvider("test_provider", ["en", "es"])
69
-
70
- assert provider.provider_name == "test_provider"
71
- assert provider.supported_languages == ["en", "es"]
72
- assert isinstance(provider._output_dir, Path)
73
- assert provider._output_dir.exists()
74
-
75
- def test_provider_initialization_no_languages(self):
76
- """Test provider initialization without supported languages."""
77
- provider = ConcreteTTSProvider("test_provider")
78
-
79
- assert provider.provider_name == "test_provider"
80
- assert provider.supported_languages == []
81
-
82
- def test_synthesize_success(self):
83
- """Test successful speech synthesis."""
84
- result = self.provider.synthesize(self.request)
85
-
86
- assert isinstance(result, AudioContent)
87
- assert result.data == b"fake_audio_data"
88
- assert result.format == "wav"
89
- assert result.sample_rate == 44100
90
- assert result.duration > 0
91
- assert "test_" in result.filename
92
-
93
- def test_synthesize_with_language_validation(self):
94
- """Test synthesis with language validation."""
95
- provider = ConcreteTTSProvider("test", ["en", "es"])
96
-
97
- # Valid language should work
98
- result = provider.synthesize(self.request)
99
- assert isinstance(result, AudioContent)
100
-
101
- # Invalid language should fail
102
- invalid_request = SpeechSynthesisRequest(
103
- text_content=TextContent(text="Hola", language="fr"),
104
- voice_settings=self.voice_settings
105
- )
106
-
107
- with pytest.raises(SpeechSynthesisException, match="Language fr not supported"):
108
- provider.synthesize(invalid_request)
109
-
110
- def test_synthesize_with_voice_validation(self):
111
- """Test synthesis with voice validation."""
112
- provider = ConcreteTTSProvider("test", voices=["voice1", "voice2"])
113
-
114
- # Valid voice should work
115
- result = provider.synthesize(self.request)
116
- assert isinstance(result, AudioContent)
117
-
118
- # Invalid voice should fail
119
- invalid_request = SpeechSynthesisRequest(
120
- text_content=self.text_content,
121
- voice_settings=VoiceSettings(voice_id="invalid_voice", speed=1.0, pitch=1.0)
122
- )
123
-
124
- with pytest.raises(SpeechSynthesisException, match="Voice invalid_voice not available"):
125
- provider.synthesize(invalid_request)
126
-
127
- def test_synthesize_empty_text_fails(self):
128
- """Test that empty text raises exception."""
129
- empty_request = SpeechSynthesisRequest(
130
- text_content=TextContent(text="", language="en"),
131
- voice_settings=self.voice_settings
132
- )
133
-
134
- with pytest.raises(SpeechSynthesisException, match="Text content cannot be empty"):
135
- self.provider.synthesize(empty_request)
136
-
137
- def test_synthesize_whitespace_text_fails(self):
138
- """Test that whitespace-only text raises exception."""
139
- whitespace_request = SpeechSynthesisRequest(
140
- text_content=TextContent(text=" ", language="en"),
141
- voice_settings=self.voice_settings
142
- )
143
-
144
- with pytest.raises(SpeechSynthesisException, match="Text content cannot be empty"):
145
- self.provider.synthesize(whitespace_request)
146
-
147
- def test_synthesize_provider_error(self):
148
- """Test handling of provider-specific errors."""
149
- self.provider.set_should_fail(True)
150
-
151
- with pytest.raises(SpeechSynthesisException, match="TTS synthesis failed"):
152
- self.provider.synthesize(self.request)
153
-
154
- def test_synthesize_stream_success(self):
155
- """Test successful streaming synthesis."""
156
- chunks = list(self.provider.synthesize_stream(self.request))
157
-
158
- assert len(chunks) == 3
159
-
160
- for i, chunk in enumerate(chunks):
161
- assert isinstance(chunk, AudioChunk)
162
- assert chunk.data == f"chunk{i+1}".encode()
163
- assert chunk.format == "wav"
164
- assert chunk.sample_rate == 44100
165
- assert chunk.chunk_index == i
166
- assert chunk.timestamp > 0
167
-
168
- # Last chunk should be final
169
- assert chunks[-1].is_final is True
170
- assert chunks[0].is_final is False
171
- assert chunks[1].is_final is False
172
-
173
- def test_synthesize_stream_provider_error(self):
174
- """Test handling of provider errors in streaming."""
175
- self.provider.set_should_fail(True)
176
-
177
- with pytest.raises(SpeechSynthesisException, match="TTS streaming synthesis failed"):
178
- list(self.provider.synthesize_stream(self.request))
179
-
180
- def test_calculate_duration(self):
181
- """Test audio duration calculation."""
182
- # Test with standard parameters
183
- audio_data = b"x" * 88200 # 1 second at 44100 Hz, 16-bit, mono
184
- duration = self.provider._calculate_duration(audio_data, 44100)
185
- assert duration == 1.0
186
-
187
- # Test with different sample rate
188
- duration = self.provider._calculate_duration(audio_data, 22050)
189
- assert duration == 2.0
190
-
191
- # Test with stereo
192
- duration = self.provider._calculate_duration(audio_data, 44100, channels=2)
193
- assert duration == 0.5
194
-
195
- # Test with empty data
196
- duration = self.provider._calculate_duration(b"", 44100)
197
- assert duration == 0.0
198
-
199
- # Test with zero sample rate
200
- duration = self.provider._calculate_duration(audio_data, 0)
201
- assert duration == 0.0
202
-
203
- def test_ensure_output_directory(self):
204
- """Test output directory creation."""
205
- output_dir = self.provider._ensure_output_directory()
206
-
207
- assert isinstance(output_dir, Path)
208
- assert output_dir.exists()
209
- assert output_dir.is_dir()
210
- assert "tts_output" in str(output_dir)
211
-
212
- def test_generate_output_path(self):
213
- """Test output path generation."""
214
- path1 = self.provider._generate_output_path()
215
- path2 = self.provider._generate_output_path()
216
-
217
- # Paths should be different (due to timestamp)
218
- assert path1 != path2
219
- assert path1.suffix == ".wav"
220
- assert path2.suffix == ".wav"
221
- assert "test_" in path1.name
222
- assert "test_" in path2.name
223
-
224
- # Test with custom prefix and extension
225
- path3 = self.provider._generate_output_path("custom", "mp3")
226
- assert path3.suffix == ".mp3"
227
- assert "custom_" in path3.name
228
-
229
- @patch('time.time')
230
- @patch('pathlib.Path.glob')
231
- @patch('pathlib.Path.stat')
232
- @patch('pathlib.Path.unlink')
233
- def test_cleanup_temp_files(self, mock_unlink, mock_stat, mock_glob, mock_time):
234
- """Test temporary file cleanup."""
235
- # Mock current time
236
- mock_time.return_value = 1000000
237
-
238
- # Mock old file
239
- old_file = Mock()
240
- old_file.is_file.return_value = True
241
- old_file.stat.return_value.st_mtime = 900000 # 100000 seconds old
242
-
243
- # Mock recent file
244
- recent_file = Mock()
245
- recent_file.is_file.return_value = True
246
- recent_file.stat.return_value.st_mtime = 999000 # 1000 seconds old
247
-
248
- mock_glob.return_value = [old_file, recent_file]
249
-
250
- # Cleanup with 24 hour limit (86400 seconds)
251
- self.provider._cleanup_temp_files(24)
252
-
253
- # Old file should be deleted, recent file should not
254
- old_file.unlink.assert_called_once()
255
- recent_file.unlink.assert_not_called()
256
-
257
- def test_cleanup_temp_files_error_handling(self):
258
- """Test cleanup error handling."""
259
- # Should not raise exception even if cleanup fails
260
- with patch.object(self.provider._output_dir, 'glob', side_effect=Exception("Test error")):
261
- self.provider._cleanup_temp_files() # Should not raise
262
-
263
- def test_handle_provider_error(self):
264
- """Test provider error handling."""
265
- original_error = ValueError("Original error")
266
-
267
- with pytest.raises(SpeechSynthesisException) as exc_info:
268
- self.provider._handle_provider_error(original_error, "testing")
269
-
270
- assert "test error during testing: Original error" in str(exc_info.value)
271
- assert exc_info.value.__cause__ is original_error
272
-
273
- def test_handle_provider_error_no_context(self):
274
- """Test provider error handling without context."""
275
- original_error = ValueError("Original error")
276
-
277
- with pytest.raises(SpeechSynthesisException) as exc_info:
278
- self.provider._handle_provider_error(original_error)
279
-
280
- assert "test error: Original error" in str(exc_info.value)
281
- assert exc_info.value.__cause__ is original_error
282
-
283
- def test_abstract_methods_not_implemented(self):
284
- """Test that abstract methods raise NotImplementedError."""
285
- # Create instance of base class directly (should fail)
286
- with pytest.raises(TypeError):
287
- TTSProviderBase("test")
288
-
289
- def test_provider_unavailable(self):
290
- """Test behavior when provider is unavailable."""
291
- provider = ConcreteTTSProvider(available=False)
292
- assert provider.is_available() is False
293
-
294
- def test_no_voices_available(self):
295
- """Test behavior when no voices are available."""
296
- provider = ConcreteTTSProvider(voices=[])
297
- assert provider.get_available_voices() == []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/infrastructure/config/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Configuration unit tests."""
 
 
tests/unit/infrastructure/config/test_dependency_container.py DELETED
@@ -1,539 +0,0 @@
1
- """Unit tests for DependencyContainer."""
2
-
3
- import pytest
4
- from unittest.mock import Mock, patch, MagicMock
5
- from threading import Thread
6
- import time
7
-
8
- from src.infrastructure.config.dependency_container import (
9
- DependencyContainer,
10
- DependencyScope,
11
- ServiceDescriptor,
12
- ServiceLifetime,
13
- get_container,
14
- set_container,
15
- cleanup_container
16
- )
17
- from src.infrastructure.config.app_config import AppConfig
18
- from src.infrastructure.tts.provider_factory import TTSProviderFactory
19
- from src.infrastructure.stt.provider_factory import STTProviderFactory
20
- from src.infrastructure.translation.provider_factory import TranslationProviderFactory, TranslationProviderType
21
- from src.domain.interfaces.speech_synthesis import ISpeechSynthesisService
22
- from src.domain.interfaces.speech_recognition import ISpeechRecognitionService
23
- from src.domain.interfaces.translation import ITranslationService
24
-
25
-
26
- class MockService:
27
- """Mock service for testing."""
28
-
29
- def __init__(self, name="mock", **kwargs):
30
- self.name = name
31
- self.kwargs = kwargs
32
- self.cleanup_called = False
33
-
34
- def cleanup(self):
35
- self.cleanup_called = True
36
-
37
-
38
- class MockServiceWithDispose:
39
- """Mock service with dispose method."""
40
-
41
- def __init__(self, name="mock"):
42
- self.name = name
43
- self.dispose_called = False
44
-
45
- def dispose(self):
46
- self.dispose_called = True
47
-
48
-
49
- def mock_factory(**kwargs):
50
- """Mock factory function."""
51
- return MockService("factory_created", **kwargs)
52
-
53
-
54
- class TestServiceDescriptor:
55
- """Test cases for ServiceDescriptor."""
56
-
57
- def test_service_descriptor_creation(self):
58
- """Test service descriptor creation."""
59
- descriptor = ServiceDescriptor(
60
- service_type=MockService,
61
- implementation=MockService,
62
- lifetime=ServiceLifetime.SINGLETON,
63
- factory_args={'name': 'test'}
64
- )
65
-
66
- assert descriptor.service_type == MockService
67
- assert descriptor.implementation == MockService
68
- assert descriptor.lifetime == ServiceLifetime.SINGLETON
69
- assert descriptor.factory_args == {'name': 'test'}
70
-
71
- def test_service_descriptor_defaults(self):
72
- """Test service descriptor with default values."""
73
- descriptor = ServiceDescriptor(
74
- service_type=MockService,
75
- implementation=MockService
76
- )
77
-
78
- assert descriptor.lifetime == ServiceLifetime.TRANSIENT
79
- assert descriptor.factory_args == {}
80
-
81
-
82
- class TestDependencyContainer:
83
- """Test cases for DependencyContainer."""
84
-
85
- def setup_method(self):
86
- """Set up test fixtures."""
87
- self.container = DependencyContainer()
88
-
89
- def teardown_method(self):
90
- """Clean up after tests."""
91
- self.container.cleanup()
92
-
93
- def test_container_initialization(self):
94
- """Test container initialization."""
95
- assert isinstance(self.container._config, AppConfig)
96
- assert isinstance(self.container._services, dict)
97
- assert isinstance(self.container._singletons, dict)
98
- assert isinstance(self.container._scoped_instances, dict)
99
-
100
- # Should have default services registered
101
- assert AppConfig in self.container._singletons
102
-
103
- def test_container_initialization_with_config(self):
104
- """Test container initialization with custom config."""
105
- config = AppConfig()
106
- container = DependencyContainer(config)
107
-
108
- assert container._config is config
109
- assert AppConfig in container._singletons
110
- assert container._singletons[AppConfig] is config
111
-
112
- def test_register_singleton_class(self):
113
- """Test registering singleton service with class."""
114
- self.container.register_singleton(MockService, MockService, {'name': 'test'})
115
-
116
- assert MockService in self.container._services
117
- descriptor = self.container._services[MockService]
118
- assert descriptor.lifetime == ServiceLifetime.SINGLETON
119
- assert descriptor.factory_args == {'name': 'test'}
120
-
121
- def test_register_singleton_instance(self):
122
- """Test registering singleton service with instance."""
123
- instance = MockService("test_instance")
124
- self.container.register_singleton(MockService, instance)
125
-
126
- assert MockService in self.container._singletons
127
- assert self.container._singletons[MockService] is instance
128
-
129
- def test_register_singleton_factory(self):
130
- """Test registering singleton service with factory function."""
131
- self.container.register_singleton(MockService, mock_factory, {'name': 'factory_test'})
132
-
133
- service = self.container.resolve(MockService)
134
- assert isinstance(service, MockService)
135
- assert service.name == "factory_created"
136
- assert service.kwargs == {'name': 'factory_test'}
137
-
138
- def test_register_transient(self):
139
- """Test registering transient service."""
140
- self.container.register_transient(MockService, MockService, {'name': 'transient'})
141
-
142
- assert MockService in self.container._services
143
- descriptor = self.container._services[MockService]
144
- assert descriptor.lifetime == ServiceLifetime.TRANSIENT
145
-
146
- def test_register_scoped(self):
147
- """Test registering scoped service."""
148
- self.container.register_scoped(MockService, MockService, {'name': 'scoped'})
149
-
150
- assert MockService in self.container._services
151
- descriptor = self.container._services[MockService]
152
- assert descriptor.lifetime == ServiceLifetime.SCOPED
153
-
154
- def test_resolve_singleton(self):
155
- """Test resolving singleton service."""
156
- self.container.register_singleton(MockService, MockService, {'name': 'singleton'})
157
-
158
- service1 = self.container.resolve(MockService)
159
- service2 = self.container.resolve(MockService)
160
-
161
- assert service1 is service2
162
- assert service1.name == 'singleton'
163
-
164
- def test_resolve_transient(self):
165
- """Test resolving transient service."""
166
- self.container.register_transient(MockService, MockService, {'name': 'transient'})
167
-
168
- service1 = self.container.resolve(MockService)
169
- service2 = self.container.resolve(MockService)
170
-
171
- assert service1 is not service2
172
- assert service1.name == 'transient'
173
- assert service2.name == 'transient'
174
-
175
- def test_resolve_scoped(self):
176
- """Test resolving scoped service."""
177
- self.container.register_scoped(MockService, MockService, {'name': 'scoped'})
178
-
179
- service1 = self.container.resolve(MockService)
180
- service2 = self.container.resolve(MockService)
181
-
182
- assert service1 is service2 # Same instance within scope
183
- assert service1.name == 'scoped'
184
-
185
- def test_resolve_unregistered_service(self):
186
- """Test resolving unregistered service raises error."""
187
- class UnregisteredService:
188
- pass
189
-
190
- with pytest.raises(ValueError, match="Service UnregisteredService is not registered"):
191
- self.container.resolve(UnregisteredService)
192
-
193
- def test_resolve_service_creation_error(self):
194
- """Test handling service creation errors."""
195
- def failing_factory():
196
- raise Exception("Creation failed")
197
-
198
- self.container.register_singleton(MockService, failing_factory)
199
-
200
- with pytest.raises(Exception, match="Creation failed"):
201
- self.container.resolve(MockService)
202
-
203
- def test_thread_safety(self):
204
- """Test container thread safety."""
205
- self.container.register_singleton(MockService, MockService, {'name': 'thread_test'})
206
-
207
- results = []
208
-
209
- def resolve_service():
210
- service = self.container.resolve(MockService)
211
- results.append(service)
212
-
213
- threads = [Thread(target=resolve_service) for _ in range(10)]
214
-
215
- for thread in threads:
216
- thread.start()
217
-
218
- for thread in threads:
219
- thread.join()
220
-
221
- # All threads should get the same singleton instance
222
- assert len(results) == 10
223
- assert all(service is results[0] for service in results)
224
-
225
- def test_get_tts_provider_default(self):
226
- """Test getting TTS provider with default settings."""
227
- with patch.object(TTSProviderFactory, 'get_provider_with_fallback') as mock_get:
228
- mock_provider = Mock()
229
- mock_get.return_value = mock_provider
230
-
231
- provider = self.container.get_tts_provider()
232
-
233
- assert provider is mock_provider
234
- mock_get.assert_called_once()
235
-
236
- def test_get_tts_provider_specific(self):
237
- """Test getting specific TTS provider."""
238
- with patch.object(TTSProviderFactory, 'create_provider') as mock_create:
239
- mock_provider = Mock()
240
- mock_create.return_value = mock_provider
241
-
242
- provider = self.container.get_tts_provider('kokoro', lang_code='en')
243
-
244
- assert provider is mock_provider
245
- mock_create.assert_called_once_with('kokoro', lang_code='en')
246
-
247
- def test_get_stt_provider_default(self):
248
- """Test getting STT provider with default settings."""
249
- with patch.object(STTProviderFactory, 'create_provider_with_fallback') as mock_get:
250
- mock_provider = Mock()
251
- mock_get.return_value = mock_provider
252
-
253
- provider = self.container.get_stt_provider()
254
-
255
- assert provider is mock_provider
256
- mock_get.assert_called_once()
257
-
258
- def test_get_stt_provider_specific(self):
259
- """Test getting specific STT provider."""
260
- with patch.object(STTProviderFactory, 'create_provider') as mock_create:
261
- mock_provider = Mock()
262
- mock_create.return_value = mock_provider
263
-
264
- provider = self.container.get_stt_provider('whisper')
265
-
266
- assert provider is mock_provider
267
- mock_create.assert_called_once_with('whisper')
268
-
269
- def test_get_translation_provider_default(self):
270
- """Test getting translation provider with default settings."""
271
- with patch.object(TranslationProviderFactory, 'get_default_provider') as mock_get:
272
- mock_provider = Mock()
273
- mock_get.return_value = mock_provider
274
-
275
- provider = self.container.get_translation_provider()
276
-
277
- assert provider is mock_provider
278
- mock_get.assert_called_once_with(None)
279
-
280
- def test_get_translation_provider_specific(self):
281
- """Test getting specific translation provider."""
282
- with patch.object(TranslationProviderFactory, 'create_provider') as mock_create:
283
- mock_provider = Mock()
284
- mock_create.return_value = mock_provider
285
-
286
- config = {'model': 'test'}
287
- provider = self.container.get_translation_provider(TranslationProviderType.NLLB, config)
288
-
289
- assert provider is mock_provider
290
- mock_create.assert_called_once_with(TranslationProviderType.NLLB, config)
291
-
292
- def test_clear_scoped_instances(self):
293
- """Test clearing scoped instances."""
294
- self.container.register_scoped(MockService, MockService)
295
-
296
- # Create scoped instance
297
- service = self.container.resolve(MockService)
298
- assert MockService in self.container._scoped_instances
299
-
300
- self.container.clear_scoped_instances()
301
-
302
- assert len(self.container._scoped_instances) == 0
303
- assert service.cleanup_called is True
304
-
305
- def test_cleanup_instance_with_cleanup_method(self):
306
- """Test cleanup of instance with cleanup method."""
307
- instance = MockService()
308
- self.container._cleanup_instance(instance)
309
-
310
- assert instance.cleanup_called is True
311
-
312
- def test_cleanup_instance_with_dispose_method(self):
313
- """Test cleanup of instance with dispose method."""
314
- instance = MockServiceWithDispose()
315
- self.container._cleanup_instance(instance)
316
-
317
- assert instance.dispose_called is True
318
-
319
- def test_cleanup_instance_no_cleanup_method(self):
320
- """Test cleanup of instance without cleanup method."""
321
- instance = object()
322
-
323
- # Should not raise exception
324
- self.container._cleanup_instance(instance)
325
-
326
- def test_cleanup_instance_error_handling(self):
327
- """Test cleanup error handling."""
328
- instance = Mock()
329
- instance.cleanup.side_effect = Exception("Cleanup error")
330
-
331
- # Should not raise exception
332
- self.container._cleanup_instance(instance)
333
-
334
- def test_cleanup_container(self):
335
- """Test full container cleanup."""
336
- # Register services
337
- self.container.register_singleton(MockService, MockService)
338
- self.container.register_scoped(MockServiceWithDispose, MockServiceWithDispose)
339
-
340
- # Create instances
341
- singleton = self.container.resolve(MockService)
342
- scoped = self.container.resolve(MockServiceWithDispose)
343
-
344
- # Mock factories
345
- mock_tts_factory = Mock()
346
- mock_translation_factory = Mock()
347
- self.container._tts_factory = mock_tts_factory
348
- self.container._translation_factory = mock_translation_factory
349
-
350
- self.container.cleanup()
351
-
352
- # Check cleanup was called
353
- assert singleton.cleanup_called is True
354
- assert scoped.dispose_called is True
355
- mock_tts_factory.cleanup_providers.assert_called_once()
356
- mock_translation_factory.clear_cache.assert_called_once()
357
-
358
- # Check instances were cleared
359
- assert len(self.container._singletons) == 0
360
- assert len(self.container._scoped_instances) == 0
361
- assert self.container._tts_factory is None
362
- assert self.container._translation_factory is None
363
-
364
- def test_cleanup_factory_error_handling(self):
365
- """Test cleanup error handling for factories."""
366
- mock_tts_factory = Mock()
367
- mock_tts_factory.cleanup_providers.side_effect = Exception("TTS cleanup error")
368
- self.container._tts_factory = mock_tts_factory
369
-
370
- # Should not raise exception
371
- self.container.cleanup()
372
-
373
- def test_is_registered(self):
374
- """Test checking if service is registered."""
375
- assert self.container.is_registered(AppConfig) is True # Default registration
376
- assert self.container.is_registered(MockService) is False
377
-
378
- self.container.register_singleton(MockService, MockService)
379
- assert self.container.is_registered(MockService) is True
380
-
381
- def test_get_registered_services(self):
382
- """Test getting registered services info."""
383
- self.container.register_singleton(MockService, MockService)
384
- self.container.register_transient(MockServiceWithDispose, MockServiceWithDispose)
385
-
386
- services = self.container.get_registered_services()
387
-
388
- assert 'AppConfig' in services
389
- assert 'MockService' in services
390
- assert 'MockServiceWithDispose' in services
391
- assert services['MockService'] == 'singleton'
392
- assert services['MockServiceWithDispose'] == 'transient'
393
-
394
- def test_create_scope(self):
395
- """Test creating dependency scope."""
396
- scope = self.container.create_scope()
397
-
398
- assert isinstance(scope, DependencyScope)
399
- assert scope._parent is self.container
400
-
401
- def test_context_manager(self):
402
- """Test container as context manager."""
403
- with DependencyContainer() as container:
404
- container.register_singleton(MockService, MockService)
405
- service = container.resolve(MockService)
406
-
407
- assert isinstance(service, MockService)
408
-
409
- # Cleanup should have been called
410
- assert service.cleanup_called is True
411
-
412
-
413
- class TestDependencyScope:
414
- """Test cases for DependencyScope."""
415
-
416
- def setup_method(self):
417
- """Set up test fixtures."""
418
- self.container = DependencyContainer()
419
- self.scope = DependencyScope(self.container)
420
-
421
- def teardown_method(self):
422
- """Clean up after tests."""
423
- self.scope.cleanup()
424
- self.container.cleanup()
425
-
426
- def test_scope_initialization(self):
427
- """Test scope initialization."""
428
- assert self.scope._parent is self.container
429
- assert isinstance(self.scope._scoped_instances, dict)
430
-
431
- def test_resolve_singleton_from_parent(self):
432
- """Test resolving singleton from parent container."""
433
- self.container.register_singleton(MockService, MockService)
434
-
435
- service1 = self.scope.resolve(MockService)
436
- service2 = self.scope.resolve(MockService)
437
-
438
- assert service1 is service2
439
- assert isinstance(service1, MockService)
440
-
441
- def test_resolve_scoped_service(self):
442
- """Test resolving scoped service within scope."""
443
- self.container.register_scoped(MockService, MockService)
444
-
445
- service1 = self.scope.resolve(MockService)
446
- service2 = self.scope.resolve(MockService)
447
-
448
- assert service1 is service2 # Same within scope
449
- assert MockService in self.scope._scoped_instances
450
-
451
- def test_resolve_transient_service(self):
452
- """Test resolving transient service."""
453
- self.container.register_transient(MockService, MockService)
454
-
455
- service1 = self.scope.resolve(MockService)
456
- service2 = self.scope.resolve(MockService)
457
-
458
- assert service1 is not service2 # Different instances
459
-
460
- def test_scope_cleanup(self):
461
- """Test scope cleanup."""
462
- self.container.register_scoped(MockService, MockService)
463
-
464
- service = self.scope.resolve(MockService)
465
- assert MockService in self.scope._scoped_instances
466
-
467
- self.scope.cleanup()
468
-
469
- assert len(self.scope._scoped_instances) == 0
470
- assert service.cleanup_called is True
471
-
472
- def test_scope_context_manager(self):
473
- """Test scope as context manager."""
474
- self.container.register_scoped(MockService, MockService)
475
-
476
- with self.container.create_scope() as scope:
477
- service = scope.resolve(MockService)
478
- assert isinstance(service, MockService)
479
-
480
- # Cleanup should have been called
481
- assert service.cleanup_called is True
482
-
483
-
484
- class TestGlobalContainer:
485
- """Test cases for global container functions."""
486
-
487
- def teardown_method(self):
488
- """Clean up after tests."""
489
- cleanup_container()
490
-
491
- def test_get_container_creates_global(self):
492
- """Test getting global container creates it if not exists."""
493
- container = get_container()
494
-
495
- assert isinstance(container, DependencyContainer)
496
-
497
- # Second call should return same instance
498
- container2 = get_container()
499
- assert container is container2
500
-
501
- def test_set_container(self):
502
- """Test setting global container."""
503
- custom_container = DependencyContainer()
504
- set_container(custom_container)
505
-
506
- container = get_container()
507
- assert container is custom_container
508
-
509
- def test_set_container_cleans_up_previous(self):
510
- """Test setting container cleans up previous one."""
511
- # Get initial container and register service
512
- container1 = get_container()
513
- container1.register_singleton(MockService, MockService)
514
- service = container1.resolve(MockService)
515
-
516
- # Set new container
517
- container2 = DependencyContainer()
518
- set_container(container2)
519
-
520
- # Previous container should be cleaned up
521
- assert service.cleanup_called is True
522
-
523
- # New container should be active
524
- assert get_container() is container2
525
-
526
- def test_cleanup_container(self):
527
- """Test cleaning up global container."""
528
- container = get_container()
529
- container.register_singleton(MockService, MockService)
530
- service = container.resolve(MockService)
531
-
532
- cleanup_container()
533
-
534
- # Service should be cleaned up
535
- assert service.cleanup_called is True
536
-
537
- # New container should be created on next get
538
- new_container = get_container()
539
- assert new_container is not container
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/infrastructure/factories/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Factory unit tests."""
 
 
tests/unit/infrastructure/factories/test_stt_provider_factory.py DELETED
@@ -1,284 +0,0 @@
1
- """Unit tests for STTProviderFactory."""
2
-
3
- import pytest
4
- from unittest.mock import Mock, patch
5
-
6
- from src.infrastructure.stt.provider_factory import STTProviderFactory, ASRFactory
7
- from src.infrastructure.base.stt_provider_base import STTProviderBase
8
- from src.domain.exceptions import SpeechRecognitionException
9
-
10
-
11
- class MockSTTProvider(STTProviderBase):
12
- """Mock STT provider for testing."""
13
-
14
- def __init__(self, provider_name="mock", available=True, models=None):
15
- super().__init__(provider_name)
16
- self._available = available
17
- self._models = models or ["model1", "model2"]
18
-
19
- def _perform_transcription(self, audio_path, model):
20
- return "Mock transcription"
21
-
22
- def is_available(self):
23
- return self._available
24
-
25
- def get_available_models(self):
26
- return self._models
27
-
28
- def get_default_model(self):
29
- return self._models[0] if self._models else "default"
30
-
31
-
32
- class TestSTTProviderFactory:
33
- """Test cases for STTProviderFactory."""
34
-
35
- def setup_method(self):
36
- """Set up test fixtures."""
37
- # Patch the providers registry for testing
38
- self.original_providers = STTProviderFactory._providers.copy()
39
- STTProviderFactory._providers = {'mock': MockSTTProvider}
40
-
41
- def teardown_method(self):
42
- """Clean up after tests."""
43
- STTProviderFactory._providers = self.original_providers
44
-
45
- def test_create_provider_success(self):
46
- """Test successful provider creation."""
47
- with patch.object(MockSTTProvider, 'is_available', return_value=True):
48
- provider = STTProviderFactory.create_provider('mock')
49
-
50
- assert isinstance(provider, MockSTTProvider)
51
- assert provider.provider_name == 'mock'
52
-
53
- def test_create_provider_case_insensitive(self):
54
- """Test provider creation is case insensitive."""
55
- with patch.object(MockSTTProvider, 'is_available', return_value=True):
56
- provider = STTProviderFactory.create_provider('MOCK')
57
-
58
- assert isinstance(provider, MockSTTProvider)
59
-
60
- def test_create_provider_unknown(self):
61
- """Test creating unknown provider raises exception."""
62
- with pytest.raises(SpeechRecognitionException, match="Unknown STT provider: unknown"):
63
- STTProviderFactory.create_provider('unknown')
64
-
65
- def test_create_provider_unavailable(self):
66
- """Test creating unavailable provider raises exception."""
67
- with patch.object(MockSTTProvider, 'is_available', return_value=False):
68
- with pytest.raises(SpeechRecognitionException, match="STT provider mock is not available"):
69
- STTProviderFactory.create_provider('mock')
70
-
71
- def test_create_provider_creation_error(self):
72
- """Test handling provider creation errors."""
73
- with patch.object(MockSTTProvider, '__init__', side_effect=Exception("Creation error")):
74
- with pytest.raises(SpeechRecognitionException, match="Failed to create STT provider mock"):
75
- STTProviderFactory.create_provider('mock')
76
-
77
- def test_create_provider_with_fallback_success(self):
78
- """Test creating provider with fallback logic."""
79
- STTProviderFactory._providers = {
80
- 'available': MockSTTProvider,
81
- 'unavailable': MockSTTProvider
82
- }
83
-
84
- def mock_is_available(self):
85
- return self.provider_name == 'available'
86
-
87
- with patch.object(MockSTTProvider, 'is_available', mock_is_available):
88
- provider = STTProviderFactory.create_provider_with_fallback('unavailable')
89
- assert provider.provider_name == 'available'
90
-
91
- def test_create_provider_with_fallback_preferred_works(self):
92
- """Test fallback when preferred provider works."""
93
- with patch.object(MockSTTProvider, 'is_available', return_value=True):
94
- provider = STTProviderFactory.create_provider_with_fallback('mock')
95
- assert provider.provider_name == 'mock'
96
-
97
- def test_create_provider_with_fallback_none_available(self):
98
- """Test fallback when no providers are available."""
99
- with patch.object(MockSTTProvider, 'is_available', return_value=False):
100
- with pytest.raises(SpeechRecognitionException, match="No STT providers are available"):
101
- STTProviderFactory.create_provider_with_fallback('mock')
102
-
103
- def test_create_provider_with_fallback_uses_fallback_order(self):
104
- """Test that fallback uses the predefined fallback order."""
105
- STTProviderFactory._providers = {
106
- 'whisper': MockSTTProvider,
107
- 'parakeet': MockSTTProvider
108
- }
109
- STTProviderFactory._fallback_order = ['whisper', 'parakeet']
110
-
111
- def mock_is_available(self):
112
- return self.provider_name == 'parakeet' # Only parakeet is available
113
-
114
- with patch.object(MockSTTProvider, 'is_available', mock_is_available):
115
- provider = STTProviderFactory.create_provider_with_fallback('unavailable')
116
- assert provider.provider_name == 'parakeet'
117
-
118
- def test_get_available_providers(self):
119
- """Test getting list of available providers."""
120
- STTProviderFactory._providers = {
121
- 'available1': MockSTTProvider,
122
- 'available2': MockSTTProvider,
123
- 'unavailable': MockSTTProvider
124
- }
125
-
126
- def mock_is_available(self):
127
- return self.provider_name in ['available1', 'available2']
128
-
129
- with patch.object(MockSTTProvider, 'is_available', mock_is_available):
130
- available = STTProviderFactory.get_available_providers()
131
-
132
- assert 'available1' in available
133
- assert 'available2' in available
134
- assert 'unavailable' not in available
135
-
136
- def test_get_available_providers_error_handling(self):
137
- """Test error handling when checking provider availability."""
138
- def mock_init_error(self):
139
- if self.provider_name == 'error':
140
- raise Exception("Test error")
141
- super(MockSTTProvider, self).__init__(self.provider_name)
142
-
143
- STTProviderFactory._providers = {
144
- 'good': MockSTTProvider,
145
- 'error': MockSTTProvider
146
- }
147
-
148
- with patch.object(MockSTTProvider, '__init__', mock_init_error):
149
- available = STTProviderFactory.get_available_providers()
150
-
151
- # Should handle error gracefully and not include error provider
152
- assert 'error' not in available
153
-
154
- def test_get_provider_info_success(self):
155
- """Test getting provider information."""
156
- with patch.object(MockSTTProvider, 'is_available', return_value=True):
157
- info = STTProviderFactory.get_provider_info('mock')
158
-
159
- assert info is not None
160
- assert info['name'] == 'mock'
161
- assert info['available'] is True
162
- assert 'supported_languages' in info
163
- assert 'available_models' in info
164
- assert 'default_model' in info
165
-
166
- def test_get_provider_info_unavailable(self):
167
- """Test getting info for unavailable provider."""
168
- with patch.object(MockSTTProvider, 'is_available', return_value=False):
169
- info = STTProviderFactory.get_provider_info('mock')
170
-
171
- assert info['available'] is False
172
- assert info['available_models'] == []
173
- assert info['default_model'] is None
174
-
175
- def test_get_provider_info_unknown(self):
176
- """Test getting info for unknown provider."""
177
- info = STTProviderFactory.get_provider_info('unknown')
178
- assert info is None
179
-
180
- def test_get_provider_info_error(self):
181
- """Test handling errors when getting provider info."""
182
- with patch.object(MockSTTProvider, '__init__', side_effect=Exception("Test error")):
183
- info = STTProviderFactory.get_provider_info('mock')
184
-
185
- assert info['available'] is False
186
- assert info['error'] == 'Test error'
187
-
188
- def test_register_provider(self):
189
- """Test registering a new provider."""
190
- class NewProvider(STTProviderBase):
191
- def _perform_transcription(self, audio_path, model):
192
- return "New provider"
193
- def is_available(self):
194
- return True
195
- def get_available_models(self):
196
- return ["new_model"]
197
- def get_default_model(self):
198
- return "new_model"
199
-
200
- STTProviderFactory.register_provider('new', NewProvider)
201
-
202
- assert 'new' in STTProviderFactory._providers
203
- assert STTProviderFactory._providers['new'] == NewProvider
204
-
205
- def test_register_provider_case_insensitive(self):
206
- """Test provider registration is case insensitive."""
207
- class NewProvider(STTProviderBase):
208
- def _perform_transcription(self, audio_path, model):
209
- return "New provider"
210
- def is_available(self):
211
- return True
212
- def get_available_models(self):
213
- return ["new_model"]
214
- def get_default_model(self):
215
- return "new_model"
216
-
217
- STTProviderFactory.register_provider('NEW', NewProvider)
218
-
219
- assert 'new' in STTProviderFactory._providers
220
-
221
-
222
- class TestASRFactory:
223
- """Test cases for legacy ASRFactory."""
224
-
225
- def setup_method(self):
226
- """Set up test fixtures."""
227
- self.original_providers = STTProviderFactory._providers.copy()
228
- STTProviderFactory._providers = {'mock': MockSTTProvider}
229
-
230
- def teardown_method(self):
231
- """Clean up after tests."""
232
- STTProviderFactory._providers = self.original_providers
233
-
234
- def test_get_model_default(self):
235
- """Test getting model with default name."""
236
- STTProviderFactory._providers = {'parakeet': MockSTTProvider}
237
-
238
- with patch.object(MockSTTProvider, 'is_available', return_value=True):
239
- provider = ASRFactory.get_model()
240
- assert provider.provider_name == 'parakeet'
241
-
242
- def test_get_model_specific(self):
243
- """Test getting specific model."""
244
- STTProviderFactory._providers = {'whisper': MockSTTProvider}
245
-
246
- with patch.object(MockSTTProvider, 'is_available', return_value=True):
247
- provider = ASRFactory.get_model('whisper')
248
- assert provider.provider_name == 'whisper'
249
-
250
- def test_get_model_legacy_mapping(self):
251
- """Test legacy model name mapping."""
252
- STTProviderFactory._providers = {'whisper': MockSTTProvider}
253
-
254
- with patch.object(MockSTTProvider, 'is_available', return_value=True):
255
- # Test faster-whisper maps to whisper
256
- provider = ASRFactory.get_model('faster-whisper')
257
- assert provider.provider_name == 'whisper'
258
-
259
- def test_get_model_fallback(self):
260
- """Test fallback when requested model is unavailable."""
261
- STTProviderFactory._providers = {
262
- 'whisper': MockSTTProvider,
263
- 'parakeet': MockSTTProvider
264
- }
265
-
266
- def mock_is_available(self):
267
- return self.provider_name == 'parakeet' # Only parakeet available
268
-
269
- with patch.object(MockSTTProvider, 'is_available', mock_is_available):
270
- with patch.object(STTProviderFactory, 'create_provider_with_fallback') as mock_fallback:
271
- mock_fallback.return_value = MockSTTProvider('parakeet')
272
-
273
- provider = ASRFactory.get_model('whisper')
274
- mock_fallback.assert_called_once_with('whisper')
275
-
276
- def test_get_model_unknown_fallback(self):
277
- """Test fallback for unknown model names."""
278
- STTProviderFactory._providers = {'parakeet': MockSTTProvider}
279
-
280
- with patch.object(STTProviderFactory, 'create_provider_with_fallback') as mock_fallback:
281
- mock_fallback.return_value = MockSTTProvider('parakeet')
282
-
283
- provider = ASRFactory.get_model('unknown_model')
284
- mock_fallback.assert_called_once_with('unknown_model')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/infrastructure/factories/test_translation_provider_factory.py DELETED
@@ -1,346 +0,0 @@
1
- """Unit tests for TranslationProviderFactory."""
2
-
3
- import pytest
4
- from unittest.mock import Mock, patch
5
- from enum import Enum
6
-
7
- from src.infrastructure.translation.provider_factory import (
8
- TranslationProviderFactory,
9
- TranslationProviderType,
10
- create_translation_provider,
11
- get_default_translation_provider,
12
- translation_provider_factory
13
- )
14
- from src.infrastructure.base.translation_provider_base import TranslationProviderBase
15
- from src.domain.exceptions import TranslationFailedException
16
-
17
-
18
- class MockTranslationProvider(TranslationProviderBase):
19
- """Mock translation provider for testing."""
20
-
21
- def __init__(self, provider_name="mock", available=True, **kwargs):
22
- super().__init__(provider_name)
23
- self._available = available
24
- self._config = kwargs
25
-
26
- def _translate_chunk(self, text, source_language, target_language):
27
- return f"Translated: {text}"
28
-
29
- def is_available(self):
30
- return self._available
31
-
32
- def get_supported_languages(self):
33
- return {"en": ["es", "fr"], "es": ["en"]}
34
-
35
-
36
- class TestTranslationProviderFactory:
37
- """Test cases for TranslationProviderFactory."""
38
-
39
- def setup_method(self):
40
- """Set up test fixtures."""
41
- self.factory = TranslationProviderFactory()
42
-
43
- # Patch the provider registry for testing
44
- self.original_registry = TranslationProviderFactory._PROVIDER_REGISTRY.copy()
45
- TranslationProviderFactory._PROVIDER_REGISTRY = {
46
- TranslationProviderType.NLLB: MockTranslationProvider
47
- }
48
-
49
- def teardown_method(self):
50
- """Clean up after tests."""
51
- TranslationProviderFactory._PROVIDER_REGISTRY = self.original_registry
52
-
53
- def test_factory_initialization(self):
54
- """Test factory initialization."""
55
- factory = TranslationProviderFactory()
56
-
57
- assert isinstance(factory._provider_cache, dict)
58
- assert isinstance(factory._availability_cache, dict)
59
- assert len(factory._provider_cache) == 0
60
- assert len(factory._availability_cache) == 0
61
-
62
- def test_create_provider_success(self):
63
- """Test successful provider creation."""
64
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
65
- provider = self.factory.create_provider(TranslationProviderType.NLLB)
66
-
67
- assert isinstance(provider, MockTranslationProvider)
68
- assert provider.provider_name == 'mock'
69
-
70
- def test_create_provider_with_config(self):
71
- """Test provider creation with custom config."""
72
- config = {'model_name': 'custom_model', 'max_chunk_length': 500}
73
-
74
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
75
- provider = self.factory.create_provider(TranslationProviderType.NLLB, config)
76
-
77
- assert isinstance(provider, MockTranslationProvider)
78
- assert provider._config['model_name'] == 'custom_model'
79
- assert provider._config['max_chunk_length'] == 500
80
-
81
- def test_create_provider_unknown_type(self):
82
- """Test creating provider with unknown type."""
83
- # Create a new enum value that's not in registry
84
- class UnknownType(Enum):
85
- UNKNOWN = "unknown"
86
-
87
- with pytest.raises(TranslationFailedException, match="Unknown translation provider type"):
88
- self.factory.create_provider(UnknownType.UNKNOWN)
89
-
90
- def test_create_provider_creation_error(self):
91
- """Test handling provider creation errors."""
92
- with patch.object(MockTranslationProvider, '__init__', side_effect=Exception("Creation error")):
93
- with pytest.raises(TranslationFailedException, match="Failed to create nllb provider"):
94
- self.factory.create_provider(TranslationProviderType.NLLB)
95
-
96
- def test_create_provider_caching(self):
97
- """Test provider instance caching."""
98
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
99
- provider1 = self.factory.create_provider(TranslationProviderType.NLLB, use_cache=True)
100
- provider2 = self.factory.create_provider(TranslationProviderType.NLLB, use_cache=True)
101
-
102
- # Should return the same cached instance
103
- assert provider1 is provider2
104
-
105
- def test_create_provider_no_caching(self):
106
- """Test provider creation without caching."""
107
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
108
- provider1 = self.factory.create_provider(TranslationProviderType.NLLB, use_cache=False)
109
- provider2 = self.factory.create_provider(TranslationProviderType.NLLB, use_cache=False)
110
-
111
- # Should return different instances
112
- assert provider1 is not provider2
113
-
114
- def test_get_available_providers(self):
115
- """Test getting available providers."""
116
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
117
- available = self.factory.get_available_providers()
118
-
119
- assert TranslationProviderType.NLLB in available
120
-
121
- def test_get_available_providers_unavailable(self):
122
- """Test getting available providers when provider is unavailable."""
123
- with patch.object(MockTranslationProvider, 'is_available', return_value=False):
124
- available = self.factory.get_available_providers()
125
-
126
- assert TranslationProviderType.NLLB not in available
127
-
128
- def test_get_available_providers_force_check(self):
129
- """Test forcing availability check ignores cache."""
130
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
131
- # First call caches result
132
- available1 = self.factory.get_available_providers()
133
-
134
- # Change availability
135
- with patch.object(MockTranslationProvider, 'is_available', return_value=False):
136
- # Without force_check, should use cached result
137
- available2 = self.factory.get_available_providers(force_check=False)
138
- assert available1 == available2
139
-
140
- # With force_check, should get updated result
141
- available3 = self.factory.get_available_providers(force_check=True)
142
- assert available3 != available1
143
-
144
- def test_get_default_provider(self):
145
- """Test getting default provider."""
146
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
147
- provider = self.factory.get_default_provider()
148
-
149
- assert isinstance(provider, MockTranslationProvider)
150
-
151
- def test_get_default_provider_no_available(self):
152
- """Test getting default provider when none are available."""
153
- with patch.object(MockTranslationProvider, 'is_available', return_value=False):
154
- with pytest.raises(TranslationFailedException, match="No translation providers are available"):
155
- self.factory.get_default_provider()
156
-
157
- def test_get_provider_with_fallback_preferred_available(self):
158
- """Test fallback when preferred provider is available."""
159
- preferred = [TranslationProviderType.NLLB]
160
-
161
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
162
- provider = self.factory.get_provider_with_fallback(preferred)
163
-
164
- assert isinstance(provider, MockTranslationProvider)
165
-
166
- def test_get_provider_with_fallback_to_any_available(self):
167
- """Test fallback to any available provider."""
168
- # Create mock enum for testing
169
- class TestType(Enum):
170
- TEST = "test"
171
-
172
- preferred = [TestType.TEST] # Not in registry
173
-
174
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
175
- provider = self.factory.get_provider_with_fallback(preferred)
176
-
177
- # Should fallback to NLLB since it's available
178
- assert isinstance(provider, MockTranslationProvider)
179
-
180
- def test_get_provider_with_fallback_none_available(self):
181
- """Test fallback when no providers are available."""
182
- preferred = [TranslationProviderType.NLLB]
183
-
184
- with patch.object(MockTranslationProvider, 'is_available', return_value=False):
185
- with pytest.raises(TranslationFailedException, match="None of the preferred translation providers are available"):
186
- self.factory.get_provider_with_fallback(preferred)
187
-
188
- def test_clear_cache(self):
189
- """Test clearing provider cache."""
190
- # Create cached provider
191
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
192
- self.factory.create_provider(TranslationProviderType.NLLB)
193
-
194
- assert len(self.factory._provider_cache) > 0
195
- assert len(self.factory._availability_cache) > 0
196
-
197
- self.factory.clear_cache()
198
-
199
- assert len(self.factory._provider_cache) == 0
200
- assert len(self.factory._availability_cache) == 0
201
-
202
- def test_get_provider_info(self):
203
- """Test getting provider information."""
204
- info = self.factory.get_provider_info(TranslationProviderType.NLLB)
205
-
206
- assert info['type'] == 'nllb'
207
- assert info['class_name'] == 'MockTranslationProvider'
208
- assert 'available' in info
209
- assert 'default_config' in info
210
- assert 'description' in info
211
-
212
- def test_get_provider_info_unknown_type(self):
213
- """Test getting info for unknown provider type."""
214
- class UnknownType(Enum):
215
- UNKNOWN = "unknown"
216
-
217
- with pytest.raises(TranslationFailedException, match="Unknown provider type"):
218
- self.factory.get_provider_info(UnknownType.UNKNOWN)
219
-
220
- def test_get_all_providers_info(self):
221
- """Test getting information about all providers."""
222
- info = self.factory.get_all_providers_info()
223
-
224
- assert 'nllb' in info
225
- assert info['nllb']['type'] == 'nllb'
226
- assert info['nllb']['class_name'] == 'MockTranslationProvider'
227
-
228
- def test_generate_cache_key(self):
229
- """Test cache key generation."""
230
- key1 = self.factory._generate_cache_key(TranslationProviderType.NLLB, None)
231
- key2 = self.factory._generate_cache_key(TranslationProviderType.NLLB, {})
232
- key3 = self.factory._generate_cache_key(TranslationProviderType.NLLB, {'a': 1, 'b': 2})
233
- key4 = self.factory._generate_cache_key(TranslationProviderType.NLLB, {'b': 2, 'a': 1})
234
-
235
- assert key1 == key2 # None and empty dict should be same
236
- assert key3 == key4 # Order shouldn't matter
237
- assert key1 != key3 # Different configs should be different
238
-
239
- def test_register_provider(self):
240
- """Test registering new provider type."""
241
- class NewType(Enum):
242
- NEW = "new"
243
-
244
- class NewProvider(TranslationProviderBase):
245
- def _translate_chunk(self, text, source_language, target_language):
246
- return f"New: {text}"
247
- def is_available(self):
248
- return True
249
- def get_supported_languages(self):
250
- return {}
251
-
252
- TranslationProviderFactory.register_provider(
253
- NewType.NEW,
254
- NewProvider,
255
- {'default_config': 'value'}
256
- )
257
-
258
- assert NewType.NEW in TranslationProviderFactory._PROVIDER_REGISTRY
259
- assert TranslationProviderFactory._PROVIDER_REGISTRY[NewType.NEW] == NewProvider
260
- assert TranslationProviderFactory._DEFAULT_CONFIGS[NewType.NEW] == {'default_config': 'value'}
261
-
262
- def test_get_supported_provider_types(self):
263
- """Test getting supported provider types."""
264
- types = TranslationProviderFactory.get_supported_provider_types()
265
-
266
- assert TranslationProviderType.NLLB in types
267
- assert isinstance(types, list)
268
-
269
- def test_is_provider_available_caching(self):
270
- """Test provider availability caching."""
271
- with patch.object(MockTranslationProvider, 'is_available', return_value=True) as mock_available:
272
- # First call should check availability
273
- available1 = self.factory._is_provider_available(TranslationProviderType.NLLB)
274
-
275
- # Second call should use cache
276
- available2 = self.factory._is_provider_available(TranslationProviderType.NLLB)
277
-
278
- assert available1 is True
279
- assert available2 is True
280
- # Should only be called once due to caching
281
- assert mock_available.call_count == 1
282
-
283
- def test_is_provider_available_force_check(self):
284
- """Test forcing provider availability check."""
285
- with patch.object(MockTranslationProvider, 'is_available', return_value=True) as mock_available:
286
- # First call
287
- self.factory._is_provider_available(TranslationProviderType.NLLB)
288
-
289
- # Force check should ignore cache
290
- self.factory._is_provider_available(TranslationProviderType.NLLB, force_check=True)
291
-
292
- # Should be called twice
293
- assert mock_available.call_count == 2
294
-
295
- def test_is_provider_available_error_handling(self):
296
- """Test error handling in availability check."""
297
- with patch.object(MockTranslationProvider, '__init__', side_effect=Exception("Test error")):
298
- available = self.factory._is_provider_available(TranslationProviderType.NLLB)
299
-
300
- assert available is False
301
- # Should cache the error result
302
- assert self.factory._availability_cache[TranslationProviderType.NLLB] is False
303
-
304
-
305
- class TestConvenienceFunctions:
306
- """Test cases for convenience functions."""
307
-
308
- def setup_method(self):
309
- """Set up test fixtures."""
310
- # Patch the provider registry for testing
311
- self.original_registry = TranslationProviderFactory._PROVIDER_REGISTRY.copy()
312
- TranslationProviderFactory._PROVIDER_REGISTRY = {
313
- TranslationProviderType.NLLB: MockTranslationProvider
314
- }
315
-
316
- def teardown_method(self):
317
- """Clean up after tests."""
318
- TranslationProviderFactory._PROVIDER_REGISTRY = self.original_registry
319
-
320
- def test_create_translation_provider(self):
321
- """Test convenience function for creating provider."""
322
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
323
- provider = create_translation_provider()
324
-
325
- assert isinstance(provider, MockTranslationProvider)
326
-
327
- def test_create_translation_provider_with_config(self):
328
- """Test convenience function with config."""
329
- config = {'test': 'value'}
330
-
331
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
332
- provider = create_translation_provider(TranslationProviderType.NLLB, config)
333
-
334
- assert isinstance(provider, MockTranslationProvider)
335
- assert provider._config['test'] == 'value'
336
-
337
- def test_get_default_translation_provider(self):
338
- """Test convenience function for getting default provider."""
339
- with patch.object(MockTranslationProvider, 'is_available', return_value=True):
340
- provider = get_default_translation_provider()
341
-
342
- assert isinstance(provider, MockTranslationProvider)
343
-
344
- def test_global_factory_instance(self):
345
- """Test global factory instance."""
346
- assert isinstance(translation_provider_factory, TranslationProviderFactory)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/infrastructure/factories/test_tts_provider_factory.py DELETED
@@ -1,232 +0,0 @@
1
- """Unit tests for TTSProviderFactory."""
2
-
3
- import pytest
4
- from unittest.mock import Mock, patch, MagicMock
5
-
6
- from src.infrastructure.tts.provider_factory import TTSProviderFactory
7
- from src.infrastructure.base.tts_provider_base import TTSProviderBase
8
- from src.domain.exceptions import SpeechSynthesisException
9
-
10
-
11
- class MockTTSProvider(TTSProviderBase):
12
- """Mock TTS provider for testing."""
13
-
14
- def __init__(self, provider_name="mock", available=True, voices=None):
15
- super().__init__(provider_name)
16
- self._available = available
17
- self._voices = voices or ["voice1", "voice2"]
18
-
19
- def _generate_audio(self, request):
20
- return b"mock_audio", 44100
21
-
22
- def _generate_audio_stream(self, request):
23
- yield b"chunk1", 44100, False
24
- yield b"chunk2", 44100, True
25
-
26
- def is_available(self):
27
- return self._available
28
-
29
- def get_available_voices(self):
30
- return self._voices
31
-
32
-
33
- class TestTTSProviderFactory:
34
- """Test cases for TTSProviderFactory."""
35
-
36
- def setup_method(self):
37
- """Set up test fixtures."""
38
- self.factory = TTSProviderFactory()
39
-
40
- def test_factory_initialization(self):
41
- """Test factory initialization."""
42
- assert isinstance(self.factory._providers, dict)
43
- assert isinstance(self.factory._provider_instances, dict)
44
- assert 'chatterbox' in self.factory._providers
45
-
46
- @patch('src.infrastructure.tts.provider_factory.ChatterboxTTSProvider')
47
- def test_register_default_providers_chatterbox(self, mock_chatterbox):
48
- """Test registration of chatterbox provider."""
49
- factory = TTSProviderFactory()
50
-
51
- assert 'chatterbox' in factory._providers
52
- assert factory._providers['chatterbox'] == mock_chatterbox
53
-
54
- @patch('src.infrastructure.tts.chatterbox_provider.ChatterboxTTSProvider', side_effect=ImportError("Not available"))
55
- def test_register_default_providers_chatterbox_unavailable(self, mock_chatterbox):
56
- """Test handling when Chatterbox provider is not available."""
57
- with pytest.raises(SpeechSynthesisException, match="No TTS providers available"):
58
- TTSProviderFactory()
59
-
60
- @patch.object(TTSProviderFactory, '_providers', {'mock': MockTTSProvider})
61
- def test_get_available_providers(self):
62
- """Test getting available providers."""
63
- with patch.object(MockTTSProvider, 'is_available', return_value=True):
64
- available = self.factory.get_available_providers()
65
- assert 'mock' in available
66
-
67
- @patch.object(TTSProviderFactory, '_providers', {'mock': MockTTSProvider})
68
- def test_get_available_providers_unavailable(self):
69
- """Test getting available providers when provider is unavailable."""
70
- with patch.object(MockTTSProvider, 'is_available', return_value=False):
71
- available = self.factory.get_available_providers()
72
- assert 'mock' not in available
73
-
74
- @patch.object(TTSProviderFactory, '_providers', {'mock': MockTTSProvider})
75
- def test_get_available_providers_error(self):
76
- """Test handling errors when checking provider availability."""
77
- with patch.object(MockTTSProvider, '__init__', side_effect=Exception("Test error")):
78
- available = self.factory.get_available_providers()
79
- assert 'mock' not in available
80
-
81
- @patch.object(TTSProviderFactory, '_providers', {'mock': MockTTSProvider})
82
- def test_create_provider_success(self):
83
- """Test successful provider creation."""
84
- with patch.object(MockTTSProvider, 'is_available', return_value=True):
85
- provider = self.factory.create_provider('mock')
86
-
87
- assert isinstance(provider, MockTTSProvider)
88
- assert provider.provider_name == 'mock'
89
-
90
- def test_create_provider_unknown(self):
91
- """Test creating unknown provider raises exception."""
92
- with pytest.raises(SpeechSynthesisException, match="Unknown TTS provider: unknown"):
93
- self.factory.create_provider('unknown')
94
-
95
- @patch.object(TTSProviderFactory, '_providers', {'mock': MockTTSProvider})
96
- def test_create_provider_unavailable(self):
97
- """Test creating unavailable provider raises exception."""
98
- with patch.object(MockTTSProvider, 'is_available', return_value=False):
99
- with pytest.raises(SpeechSynthesisException, match="TTS provider mock is not available"):
100
- self.factory.create_provider('mock')
101
-
102
- @patch.object(TTSProviderFactory, '_providers', {'mock': MockTTSProvider})
103
- def test_create_provider_creation_error(self):
104
- """Test handling provider creation errors."""
105
- with patch.object(MockTTSProvider, '__init__', side_effect=Exception("Creation error")):
106
- with pytest.raises(SpeechSynthesisException, match="Failed to create TTS provider mock"):
107
- self.factory.create_provider('mock')
108
-
109
- @patch.object(TTSProviderFactory, '_providers', {'chatterbox': MockTTSProvider})
110
- def test_create_provider_with_lang_code(self):
111
- """Test creating provider with language code."""
112
- with patch.object(MockTTSProvider, 'is_available', return_value=True):
113
- provider = self.factory.create_provider('chatterbox', lang_code='en')
114
- assert isinstance(provider, MockTTSProvider)
115
-
116
- @patch.object(TTSProviderFactory, '_providers', {
117
- 'available1': MockTTSProvider,
118
- 'available2': MockTTSProvider,
119
- 'unavailable': MockTTSProvider
120
- })
121
- def test_get_provider_with_fallback_success(self):
122
- """Test getting provider with fallback logic."""
123
- def mock_is_available(self):
124
- return self.provider_name in ['available1', 'available2']
125
-
126
- with patch.object(MockTTSProvider, 'is_available', mock_is_available):
127
- provider = self.factory.get_provider_with_fallback(['unavailable', 'available1'])
128
- assert provider.provider_name == 'available1'
129
-
130
- @patch.object(TTSProviderFactory, '_providers', {
131
- 'available': MockTTSProvider,
132
- 'unavailable': MockTTSProvider
133
- })
134
- def test_get_provider_with_fallback_to_any_available(self):
135
- """Test fallback to any available provider."""
136
- def mock_is_available(self):
137
- return self.provider_name == 'available'
138
-
139
- with patch.object(MockTTSProvider, 'is_available', mock_is_available):
140
- provider = self.factory.get_provider_with_fallback(['unavailable'])
141
- assert provider.provider_name == 'available'
142
-
143
- @patch.object(TTSProviderFactory, '_providers', {'unavailable': MockTTSProvider})
144
- def test_get_provider_with_fallback_none_available(self):
145
- """Test fallback when no providers are available."""
146
- with patch.object(MockTTSProvider, 'is_available', return_value=False):
147
- with pytest.raises(SpeechSynthesisException, match="No TTS providers are available"):
148
- self.factory.get_provider_with_fallback(['unavailable'])
149
-
150
- @patch.object(TTSProviderFactory, '_providers', {'mock': MockTTSProvider})
151
- def test_get_provider_info_success(self):
152
- """Test getting provider information."""
153
- with patch.object(MockTTSProvider, 'is_available', return_value=True):
154
- info = self.factory.get_provider_info('mock')
155
-
156
- assert info['available'] is True
157
- assert info['name'] == 'mock'
158
- assert 'supported_languages' in info
159
- assert 'available_voices' in info
160
-
161
- @patch.object(TTSProviderFactory, '_providers', {'mock': MockTTSProvider})
162
- def test_get_provider_info_unavailable(self):
163
- """Test getting info for unavailable provider."""
164
- with patch.object(MockTTSProvider, 'is_available', return_value=False):
165
- info = self.factory.get_provider_info('mock')
166
-
167
- assert info['available'] is False
168
- assert info['available_voices'] == []
169
-
170
- def test_get_provider_info_unknown(self):
171
- """Test getting info for unknown provider."""
172
- info = self.factory.get_provider_info('unknown')
173
-
174
- assert info['available'] is False
175
- assert 'error' in info
176
-
177
- @patch.object(TTSProviderFactory, '_providers', {'mock': MockTTSProvider})
178
- def test_get_provider_info_error(self):
179
- """Test handling errors when getting provider info."""
180
- with patch.object(MockTTSProvider, '__init__', side_effect=Exception("Test error")):
181
- info = self.factory.get_provider_info('mock')
182
-
183
- assert info['available'] is False
184
- assert info['error'] == 'Test error'
185
-
186
- def test_cleanup_providers(self):
187
- """Test cleaning up provider instances."""
188
- # Create mock provider with cleanup method
189
- mock_provider = Mock()
190
- mock_provider._cleanup_temp_files = Mock()
191
-
192
- self.factory._provider_instances['test'] = mock_provider
193
-
194
- self.factory.cleanup_providers()
195
-
196
- mock_provider._cleanup_temp_files.assert_called_once()
197
- assert len(self.factory._provider_instances) == 0
198
-
199
- def test_cleanup_providers_no_cleanup_method(self):
200
- """Test cleanup when provider has no cleanup method."""
201
- mock_provider = Mock()
202
- del mock_provider._cleanup_temp_files # Remove cleanup method
203
-
204
- self.factory._provider_instances['test'] = mock_provider
205
-
206
- # Should not raise exception
207
- self.factory.cleanup_providers()
208
- assert len(self.factory._provider_instances) == 0
209
-
210
- def test_cleanup_providers_cleanup_error(self):
211
- """Test handling cleanup errors."""
212
- mock_provider = Mock()
213
- mock_provider._cleanup_temp_files.side_effect = Exception("Cleanup error")
214
-
215
- self.factory._provider_instances['test'] = mock_provider
216
-
217
- # Should not raise exception
218
- self.factory.cleanup_providers()
219
- assert len(self.factory._provider_instances) == 0
220
-
221
- @patch.object(TTSProviderFactory, '_providers', {'mock': MockTTSProvider})
222
- def test_provider_instance_caching(self):
223
- """Test that provider instances are cached."""
224
- with patch.object(MockTTSProvider, 'is_available', return_value=True):
225
- # First call to get_available_providers should create instance
226
- available1 = self.factory.get_available_providers()
227
-
228
- # Second call should use cached instance
229
- available2 = self.factory.get_available_providers()
230
-
231
- assert available1 == available2
232
- assert 'mock' in self.factory._provider_instances