Michael Hu commited on
Commit
48f8a08
·
1 Parent(s): 6613cd9

Create unit tests for domain layer

Browse files
tests/unit/domain/interfaces/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Domain interface tests
tests/unit/domain/interfaces/test_audio_processing.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"kokoro_audio", format="wav", sample_rate=22050, duration=1.0)
219
+
220
+ def synthesize_stream(self, request):
221
+ yield AudioChunk(data=b"kokoro_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"dia_audio", format="wav", sample_rate=22050, duration=1.0)
226
+
227
+ def synthesize_stream(self, request):
228
+ yield AudioChunk(data=b"dia_chunk", format="wav", sample_rate=22050, chunk_index=0, is_final=True)
229
+
230
+ kokoro = KokoroImplementation()
231
+ dia = DiaImplementation()
232
+
233
+ assert isinstance(kokoro, ISpeechSynthesisService)
234
+ assert isinstance(dia, ISpeechSynthesisService)
235
+ assert type(kokoro) != type(dia)
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 ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_processing_result.py ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 CHANGED
@@ -8,342 +8,316 @@ from src.domain.models.voice_settings import VoiceSettings
8
 
9
  class TestSpeechSynthesisRequest:
10
  """Test cases for SpeechSynthesisRequest value object."""
11
-
12
- def test_valid_speech_synthesis_request_creation(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  """Test creating valid SpeechSynthesisRequest instance."""
14
- text = TextContent(text="Hello, world!", language="en")
15
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.2, language="en")
16
-
17
  request = SpeechSynthesisRequest(
18
- text=text,
19
- voice_settings=voice_settings,
20
  output_format="wav",
21
- sample_rate=44100
22
  )
23
-
24
- assert request.text == text
25
- assert request.voice_settings == voice_settings
26
  assert request.output_format == "wav"
27
- assert request.sample_rate == 44100
28
- assert request.effective_sample_rate == 44100
29
-
30
- def test_speech_synthesis_request_with_defaults(self):
31
  """Test creating SpeechSynthesisRequest with default values."""
32
- text = TextContent(text="Hello, world!", language="en")
33
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
34
-
35
  request = SpeechSynthesisRequest(
36
- text=text,
37
- voice_settings=voice_settings
38
  )
39
-
40
  assert request.output_format == "wav"
41
  assert request.sample_rate is None
42
  assert request.effective_sample_rate == 22050 # Default
43
-
44
- def test_non_text_content_raises_error(self):
45
- """Test that non-TextContent text raises TypeError."""
46
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
47
-
48
  with pytest.raises(TypeError, match="Text must be a TextContent instance"):
49
  SpeechSynthesisRequest(
50
- text="Hello, world!", # type: ignore
51
- voice_settings=voice_settings
52
  )
53
-
54
- def test_non_voice_settings_raises_error(self):
55
- """Test that non-VoiceSettings voice_settings raises TypeError."""
56
- text = TextContent(text="Hello, world!", language="en")
57
-
58
  with pytest.raises(TypeError, match="Voice settings must be a VoiceSettings instance"):
59
  SpeechSynthesisRequest(
60
- text=text,
61
- voice_settings={"voice_id": "en_male_001", "speed": 1.0} # type: ignore
62
  )
63
-
64
- def test_non_string_output_format_raises_error(self):
65
- """Test that non-string output_format raises TypeError."""
66
- text = TextContent(text="Hello, world!", language="en")
67
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
68
-
69
  with pytest.raises(TypeError, match="Output format must be a string"):
70
  SpeechSynthesisRequest(
71
- text=text,
72
- voice_settings=voice_settings,
73
  output_format=123 # type: ignore
74
  )
75
-
76
- def test_unsupported_output_format_raises_error(self):
77
- """Test that unsupported output_format raises ValueError."""
78
- text = TextContent(text="Hello, world!", language="en")
79
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
80
-
81
  with pytest.raises(ValueError, match="Unsupported output format: xyz"):
82
  SpeechSynthesisRequest(
83
- text=text,
84
- voice_settings=voice_settings,
85
  output_format="xyz"
86
  )
87
-
88
- def test_supported_output_formats(self):
89
  """Test all supported output formats."""
90
- text = TextContent(text="Hello, world!", language="en")
91
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
92
  supported_formats = ['wav', 'mp3', 'flac', 'ogg']
93
-
94
  for fmt in supported_formats:
95
  request = SpeechSynthesisRequest(
96
- text=text,
97
- voice_settings=voice_settings,
98
  output_format=fmt
99
  )
100
  assert request.output_format == fmt
101
-
102
- def test_non_integer_sample_rate_raises_error(self):
103
- """Test that non-integer sample_rate raises TypeError."""
104
- text = TextContent(text="Hello, world!", language="en")
105
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
106
-
107
  with pytest.raises(TypeError, match="Sample rate must be an integer"):
108
  SpeechSynthesisRequest(
109
- text=text,
110
- voice_settings=voice_settings,
111
- sample_rate=44100.5 # type: ignore
112
  )
113
-
114
- def test_negative_sample_rate_raises_error(self):
115
- """Test that negative sample_rate raises ValueError."""
116
- text = TextContent(text="Hello, world!", language="en")
117
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
118
-
119
  with pytest.raises(ValueError, match="Sample rate must be positive"):
120
  SpeechSynthesisRequest(
121
- text=text,
122
- voice_settings=voice_settings,
123
  sample_rate=-1
124
  )
125
-
126
- def test_zero_sample_rate_raises_error(self):
127
- """Test that zero sample_rate raises ValueError."""
128
- text = TextContent(text="Hello, world!", language="en")
129
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
130
-
131
  with pytest.raises(ValueError, match="Sample rate must be positive"):
132
  SpeechSynthesisRequest(
133
- text=text,
134
- voice_settings=voice_settings,
135
  sample_rate=0
136
  )
137
-
138
- def test_sample_rate_too_low_raises_error(self):
139
  """Test that sample rate below 8000 raises ValueError."""
140
- text = TextContent(text="Hello, world!", language="en")
141
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
142
-
143
  with pytest.raises(ValueError, match="Sample rate must be between 8000 and 192000 Hz"):
144
  SpeechSynthesisRequest(
145
- text=text,
146
- voice_settings=voice_settings,
147
  sample_rate=7999
148
  )
149
-
150
- def test_sample_rate_too_high_raises_error(self):
151
  """Test that sample rate above 192000 raises ValueError."""
152
- text = TextContent(text="Hello, world!", language="en")
153
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
154
-
155
  with pytest.raises(ValueError, match="Sample rate must be between 8000 and 192000 Hz"):
156
  SpeechSynthesisRequest(
157
- text=text,
158
- voice_settings=voice_settings,
159
  sample_rate=192001
160
  )
161
-
162
- def test_valid_sample_rate_boundaries(self):
163
  """Test valid sample rate boundaries."""
164
- text = TextContent(text="Hello, world!", language="en")
165
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
166
-
167
  # Test minimum valid sample rate
168
  request_min = SpeechSynthesisRequest(
169
- text=text,
170
- voice_settings=voice_settings,
171
  sample_rate=8000
172
  )
173
  assert request_min.sample_rate == 8000
174
-
175
  # Test maximum valid sample rate
176
  request_max = SpeechSynthesisRequest(
177
- text=text,
178
- voice_settings=voice_settings,
179
  sample_rate=192000
180
  )
181
  assert request_max.sample_rate == 192000
182
-
183
- def test_mismatched_languages_raises_error(self):
184
- """Test that mismatched text and voice languages raise ValueError."""
185
- text = TextContent(text="Hello, world!", language="en")
186
- voice_settings = VoiceSettings(voice_id="fr_male_001", speed=1.0, language="fr")
187
-
188
- with pytest.raises(ValueError, match="Text language \\(en\\) must match voice language \\(fr\\)"):
189
  SpeechSynthesisRequest(
190
- text=text,
191
- voice_settings=voice_settings
192
  )
193
-
194
  def test_matching_languages_success(self):
195
- """Test that matching text and voice languages work correctly."""
196
- text = TextContent(text="Bonjour le monde!", language="fr")
197
- voice_settings = VoiceSettings(voice_id="fr_male_001", speed=1.0, language="fr")
198
-
199
  request = SpeechSynthesisRequest(
200
- text=text,
201
  voice_settings=voice_settings
202
  )
203
-
204
- assert request.text.language == "fr"
205
- assert request.voice_settings.language == "fr"
206
-
207
- def test_estimated_duration_seconds_property(self):
208
  """Test estimated_duration_seconds property calculation."""
209
- text = TextContent(text="Hello world test", language="en") # 3 words
210
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
211
-
212
  request = SpeechSynthesisRequest(
213
- text=text,
214
- voice_settings=voice_settings
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  )
216
-
217
- # 3 words at 175 words per minute = 3/175 * 60 ≈ 1.03 seconds
218
- estimated = request.estimated_duration_seconds
219
- assert 1.0 <= estimated <= 1.1
220
-
221
- def test_estimated_duration_with_speed_adjustment(self):
222
- """Test estimated duration with different speed settings."""
223
- text = TextContent(text="Hello world test", language="en") # 3 words
224
- voice_settings_slow = VoiceSettings(voice_id="en_male_001", speed=0.5, language="en")
225
- voice_settings_fast = VoiceSettings(voice_id="en_male_001", speed=2.0, language="en")
226
-
227
- request_slow = SpeechSynthesisRequest(text=text, voice_settings=voice_settings_slow)
228
- request_fast = SpeechSynthesisRequest(text=text, voice_settings=voice_settings_fast)
229
-
230
- # Slower speed should result in longer duration
231
- assert request_slow.estimated_duration_seconds > request_fast.estimated_duration_seconds
232
-
233
- def test_is_long_text_property(self):
234
  """Test is_long_text property."""
235
- short_text = TextContent(text="Hello world", language="en")
236
- long_text = TextContent(text="a" * 5001, language="en")
237
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
238
-
239
- request_short = SpeechSynthesisRequest(text=short_text, voice_settings=voice_settings)
240
- request_long = SpeechSynthesisRequest(text=long_text, voice_settings=voice_settings)
241
-
242
- assert request_short.is_long_text is False
243
- assert request_long.is_long_text is True
244
-
245
- def test_effective_sample_rate_property(self):
246
- """Test effective_sample_rate property."""
247
- text = TextContent(text="Hello, world!", language="en")
248
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
249
-
250
- # With explicit sample rate
251
- request_explicit = SpeechSynthesisRequest(
252
- text=text,
253
- voice_settings=voice_settings,
254
- sample_rate=44100
255
  )
256
- assert request_explicit.effective_sample_rate == 44100
257
-
258
- # Without explicit sample rate (default)
259
- request_default = SpeechSynthesisRequest(
260
- text=text,
261
- voice_settings=voice_settings
 
262
  )
263
- assert request_default.effective_sample_rate == 22050
264
-
265
- def test_with_output_format_method(self):
266
  """Test with_output_format method creates new instance."""
267
- text = TextContent(text="Hello, world!", language="en")
268
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
269
-
270
  original = SpeechSynthesisRequest(
271
- text=text,
272
- voice_settings=voice_settings,
273
  output_format="wav",
274
- sample_rate=44100
275
  )
276
-
277
  new_request = original.with_output_format("mp3")
278
-
279
  assert new_request.output_format == "mp3"
280
- assert new_request.text == original.text
281
  assert new_request.voice_settings == original.voice_settings
282
  assert new_request.sample_rate == original.sample_rate
283
  assert new_request is not original # Different instances
284
-
285
- def test_with_sample_rate_method(self):
286
  """Test with_sample_rate method creates new instance."""
287
- text = TextContent(text="Hello, world!", language="en")
288
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
289
-
290
  original = SpeechSynthesisRequest(
291
- text=text,
292
- voice_settings=voice_settings,
293
- sample_rate=44100
294
  )
295
-
296
- new_request = original.with_sample_rate(22050)
297
-
298
- assert new_request.sample_rate == 22050
299
- assert new_request.text == original.text
300
  assert new_request.voice_settings == original.voice_settings
301
  assert new_request.output_format == original.output_format
302
  assert new_request is not original # Different instances
303
-
304
- def test_with_sample_rate_none(self):
305
  """Test with_sample_rate method with None value."""
306
- text = TextContent(text="Hello, world!", language="en")
307
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
308
-
309
  original = SpeechSynthesisRequest(
310
- text=text,
311
- voice_settings=voice_settings,
312
- sample_rate=44100
313
  )
314
-
315
  new_request = original.with_sample_rate(None)
316
  assert new_request.sample_rate is None
317
- assert new_request.effective_sample_rate == 22050
318
-
319
- def test_with_voice_settings_method(self):
320
  """Test with_voice_settings method creates new instance."""
321
- text = TextContent(text="Hello, world!", language="en")
322
- original_voice = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
323
- new_voice = VoiceSettings(voice_id="en_female_001", speed=1.5, language="en")
324
-
325
  original = SpeechSynthesisRequest(
326
- text=text,
327
- voice_settings=original_voice
328
  )
329
-
330
- new_request = original.with_voice_settings(new_voice)
331
-
332
- assert new_request.voice_settings == new_voice
333
- assert new_request.text == original.text
334
  assert new_request.output_format == original.output_format
335
  assert new_request.sample_rate == original.sample_rate
336
  assert new_request is not original # Different instances
337
-
338
- def test_speech_synthesis_request_is_immutable(self):
339
  """Test that SpeechSynthesisRequest is immutable (frozen dataclass)."""
340
- text = TextContent(text="Hello, world!", language="en")
341
- voice_settings = VoiceSettings(voice_id="en_male_001", speed=1.0, language="en")
342
-
343
  request = SpeechSynthesisRequest(
344
- text=text,
345
- voice_settings=voice_settings
346
  )
347
-
348
  with pytest.raises(AttributeError):
349
- request.output_format = "mp3" # type: ignore
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_translation_request.py CHANGED
@@ -7,266 +7,237 @@ from src.domain.models.text_content import TextContent
7
 
8
  class TestTranslationRequest:
9
  """Test cases for TranslationRequest value object."""
10
-
11
- def test_valid_translation_request_creation(self):
 
 
 
 
 
 
 
 
12
  """Test creating valid TranslationRequest instance."""
13
- source_text = TextContent(text="Hello, world!", language="en")
14
  request = TranslationRequest(
15
- source_text=source_text,
16
- target_language="fr",
17
  source_language="en"
18
  )
19
-
20
- assert request.source_text == source_text
21
- assert request.target_language == "fr"
22
  assert request.source_language == "en"
23
  assert request.effective_source_language == "en"
24
  assert request.is_auto_detect_source is False
25
-
26
- def test_translation_request_without_source_language(self):
27
  """Test creating TranslationRequest without explicit source language."""
28
- source_text = TextContent(text="Hello, world!", language="en")
29
  request = TranslationRequest(
30
- source_text=source_text,
31
- target_language="fr"
32
  )
33
-
34
  assert request.source_language is None
35
  assert request.effective_source_language == "en" # From TextContent
36
  assert request.is_auto_detect_source is True
37
-
38
  def test_non_text_content_source_raises_error(self):
39
- """Test that non-TextContent source_text raises TypeError."""
40
  with pytest.raises(TypeError, match="Source text must be a TextContent instance"):
41
  TranslationRequest(
42
- source_text="Hello, world!", # type: ignore
43
- target_language="fr"
44
  )
45
-
46
- def test_non_string_target_language_raises_error(self):
47
- """Test that non-string target_language raises TypeError."""
48
- source_text = TextContent(text="Hello, world!", language="en")
49
  with pytest.raises(TypeError, match="Target language must be a string"):
50
  TranslationRequest(
51
- source_text=source_text,
52
  target_language=123 # type: ignore
53
  )
54
-
55
- def test_empty_target_language_raises_error(self):
56
- """Test that empty target_language raises ValueError."""
57
- source_text = TextContent(text="Hello, world!", language="en")
58
  with pytest.raises(ValueError, match="Target language cannot be empty"):
59
  TranslationRequest(
60
- source_text=source_text,
61
  target_language=""
62
  )
63
-
64
- def test_whitespace_target_language_raises_error(self):
65
- """Test that whitespace-only target_language raises ValueError."""
66
- source_text = TextContent(text="Hello, world!", language="en")
67
  with pytest.raises(ValueError, match="Target language cannot be empty"):
68
  TranslationRequest(
69
- source_text=source_text,
70
  target_language=" "
71
  )
72
-
73
- def test_invalid_target_language_format_raises_error(self):
74
  """Test that invalid target language format raises ValueError."""
75
- source_text = TextContent(text="Hello, world!", language="en")
76
- invalid_codes = ["f", "FRA", "fr-us", "fr-USA", "123", "fr_FR"]
77
-
78
  for code in invalid_codes:
79
  with pytest.raises(ValueError, match="Invalid target language code format"):
80
  TranslationRequest(
81
- source_text=source_text,
82
  target_language=code
83
  )
84
-
85
- def test_valid_target_language_codes(self):
86
  """Test valid target language code formats."""
87
- source_text = TextContent(text="Hello, world!", language="en")
88
- valid_codes = ["fr", "de", "es", "zh", "ja", "fr-FR", "de-DE", "zh-CN"]
89
-
90
  for code in valid_codes:
91
  request = TranslationRequest(
92
- source_text=source_text,
93
  target_language=code
94
  )
95
  assert request.target_language == code
96
-
97
- def test_non_string_source_language_raises_error(self):
98
- """Test that non-string source_language raises TypeError."""
99
- source_text = TextContent(text="Hello, world!", language="en")
100
  with pytest.raises(TypeError, match="Source language must be a string"):
101
  TranslationRequest(
102
- source_text=source_text,
103
- target_language="fr",
104
  source_language=123 # type: ignore
105
  )
106
-
107
- def test_empty_source_language_raises_error(self):
108
- """Test that empty source_language raises ValueError."""
109
- source_text = TextContent(text="Hello, world!", language="en")
110
  with pytest.raises(ValueError, match="Source language cannot be empty string"):
111
  TranslationRequest(
112
- source_text=source_text,
113
- target_language="fr",
114
  source_language=""
115
  )
116
-
117
- def test_whitespace_source_language_raises_error(self):
118
- """Test that whitespace-only source_language raises ValueError."""
119
- source_text = TextContent(text="Hello, world!", language="en")
120
  with pytest.raises(ValueError, match="Source language cannot be empty string"):
121
  TranslationRequest(
122
- source_text=source_text,
123
- target_language="fr",
124
  source_language=" "
125
  )
126
-
127
- def test_invalid_source_language_format_raises_error(self):
128
  """Test that invalid source language format raises ValueError."""
129
- source_text = TextContent(text="Hello, world!", language="en")
130
  invalid_codes = ["e", "ENG", "en-us", "en-USA", "123", "en_US"]
131
-
132
  for code in invalid_codes:
133
  with pytest.raises(ValueError, match="Invalid source language code format"):
134
  TranslationRequest(
135
- source_text=source_text,
136
- target_language="fr",
137
  source_language=code
138
  )
139
-
140
- def test_same_source_and_target_language_raises_error(self):
141
- """Test that same source and target languages raise ValueError."""
142
- source_text = TextContent(text="Hello, world!", language="en")
143
-
144
- # Explicit source language same as target
145
  with pytest.raises(ValueError, match="Source and target languages cannot be the same"):
146
  TranslationRequest(
147
- source_text=source_text,
148
  target_language="en",
149
  source_language="en"
150
  )
151
-
152
- # Implicit source language (from TextContent) same as target
 
 
 
153
  with pytest.raises(ValueError, match="Source and target languages cannot be the same"):
154
  TranslationRequest(
155
- source_text=source_text,
156
- target_language="en"
157
  )
158
-
159
- def test_effective_source_language_property(self):
160
- """Test effective_source_language property."""
161
- source_text = TextContent(text="Hello, world!", language="en")
162
-
163
- # With explicit source language
164
- request_explicit = TranslationRequest(
165
- source_text=source_text,
166
- target_language="fr",
167
- source_language="de"
168
- )
169
- assert request_explicit.effective_source_language == "de"
170
-
171
- # Without explicit source language (uses TextContent language)
172
- request_implicit = TranslationRequest(
173
- source_text=source_text,
174
- target_language="fr"
175
- )
176
- assert request_implicit.effective_source_language == "en"
177
-
178
- def test_text_length_property(self):
179
  """Test text_length property."""
180
- source_text = TextContent(text="Hello, world!", language="en")
181
  request = TranslationRequest(
182
- source_text=source_text,
183
- target_language="fr"
184
  )
185
-
186
- assert request.text_length == len("Hello, world!")
187
-
188
- def test_word_count_property(self):
189
  """Test word_count property."""
190
- source_text = TextContent(text="Hello, world!", language="en")
191
  request = TranslationRequest(
192
- source_text=source_text,
193
- target_language="fr"
194
  )
195
-
196
- assert request.word_count == source_text.word_count
197
-
198
- def test_with_target_language_method(self):
199
  """Test with_target_language method creates new instance."""
200
- source_text = TextContent(text="Hello, world!", language="en")
201
  original = TranslationRequest(
202
- source_text=source_text,
203
- target_language="fr",
204
  source_language="en"
205
  )
206
-
207
- new_request = original.with_target_language("de")
208
-
209
- assert new_request.target_language == "de"
210
  assert new_request.source_text == original.source_text
211
  assert new_request.source_language == original.source_language
212
  assert new_request is not original # Different instances
213
-
214
- def test_with_source_language_method(self):
215
  """Test with_source_language method creates new instance."""
216
- source_text = TextContent(text="Hello, world!", language="en")
217
  original = TranslationRequest(
218
- source_text=source_text,
219
- target_language="fr",
220
  source_language="en"
221
  )
222
-
223
  new_request = original.with_source_language("de")
224
-
225
  assert new_request.source_language == "de"
226
  assert new_request.target_language == original.target_language
227
  assert new_request.source_text == original.source_text
228
  assert new_request is not original # Different instances
229
-
230
- def test_with_source_language_none(self):
231
  """Test with_source_language method with None value."""
232
- source_text = TextContent(text="Hello, world!", language="en")
233
  original = TranslationRequest(
234
- source_text=source_text,
235
- target_language="fr",
236
  source_language="en"
237
  )
238
-
239
  new_request = original.with_source_language(None)
240
  assert new_request.source_language is None
241
  assert new_request.is_auto_detect_source is True
242
-
243
- def test_translation_request_is_immutable(self):
244
  """Test that TranslationRequest is immutable (frozen dataclass)."""
245
- source_text = TextContent(text="Hello, world!", language="en")
246
  request = TranslationRequest(
247
- source_text=source_text,
248
- target_language="fr"
249
  )
250
-
251
  with pytest.raises(AttributeError):
252
- request.target_language = "de" # type: ignore
253
-
254
- def test_valid_language_combinations(self):
255
- """Test various valid language combinations."""
256
- test_cases = [
257
- ("en", "fr", "en"), # English to French with explicit source
258
- ("en", "de", None), # English to German with auto-detect
259
- ("fr-FR", "en-US", "fr"), # Regional codes
260
- ("zh", "ja", "zh-CN"), # Asian languages
261
- ]
262
-
263
- for text_lang, target_lang, source_lang in test_cases:
264
- source_text = TextContent(text="Test text", language=text_lang)
265
- request = TranslationRequest(
266
- source_text=source_text,
267
- target_language=target_lang,
268
- source_language=source_lang
269
- )
270
-
271
- assert request.target_language == target_lang
272
- assert request.source_language == source_lang
 
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/test_exceptions.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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