Spaces:
Build error
Build error
Michael Hu
commited on
Commit
·
acd758a
1
Parent(s):
48f8a08
Create unit tests for application layer
Browse files- tests/unit/application/__init__.py +1 -0
- tests/unit/application/dtos/__init__.py +1 -0
- tests/unit/application/dtos/test_audio_upload_dto.py +245 -0
- tests/unit/application/dtos/test_dto_validation.py +319 -0
- tests/unit/application/dtos/test_processing_request_dto.py +383 -0
- tests/unit/application/dtos/test_processing_result_dto.py +436 -0
- tests/unit/application/services/__init__.py +1 -0
- tests/unit/application/services/test_audio_processing_service.py +475 -0
- tests/unit/application/services/test_configuration_service.py +572 -0
tests/unit/application/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
"""Unit tests for application layer"""
|
tests/unit/application/dtos/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
"""Unit tests for application DTOs"""
|
tests/unit/application/dtos/test_audio_upload_dto.py
ADDED
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Unit tests for AudioUploadDto"""
|
2 |
+
|
3 |
+
import pytest
|
4 |
+
import os
|
5 |
+
|
6 |
+
from src.application.dtos.audio_upload_dto import AudioUploadDto
|
7 |
+
|
8 |
+
|
9 |
+
class TestAudioUploadDto:
|
10 |
+
"""Test cases for AudioUploadDto"""
|
11 |
+
|
12 |
+
def test_valid_audio_upload_dto(self):
|
13 |
+
"""Test creating a valid AudioUploadDto"""
|
14 |
+
filename = "test_audio.wav"
|
15 |
+
content = b"fake_audio_content_" + b"x" * 1000 # 1KB+ of fake audio
|
16 |
+
content_type = "audio/wav"
|
17 |
+
|
18 |
+
dto = AudioUploadDto(
|
19 |
+
filename=filename,
|
20 |
+
content=content,
|
21 |
+
content_type=content_type
|
22 |
+
)
|
23 |
+
|
24 |
+
assert dto.filename == filename
|
25 |
+
assert dto.content == content
|
26 |
+
assert dto.content_type == content_type
|
27 |
+
assert dto.size == len(content)
|
28 |
+
|
29 |
+
def test_audio_upload_dto_with_explicit_size(self):
|
30 |
+
"""Test creating AudioUploadDto with explicit size"""
|
31 |
+
filename = "test_audio.mp3"
|
32 |
+
content = b"fake_audio_content_" + b"x" * 2000
|
33 |
+
content_type = "audio/mpeg"
|
34 |
+
size = 2500
|
35 |
+
|
36 |
+
dto = AudioUploadDto(
|
37 |
+
filename=filename,
|
38 |
+
content=content,
|
39 |
+
content_type=content_type,
|
40 |
+
size=size
|
41 |
+
)
|
42 |
+
|
43 |
+
assert dto.size == size # Should use explicit size, not calculated
|
44 |
+
|
45 |
+
def test_empty_filename_validation(self):
|
46 |
+
"""Test validation with empty filename"""
|
47 |
+
with pytest.raises(ValueError, match="Filename cannot be empty"):
|
48 |
+
AudioUploadDto(
|
49 |
+
filename="",
|
50 |
+
content=b"fake_audio_content_" + b"x" * 1000,
|
51 |
+
content_type="audio/wav"
|
52 |
+
)
|
53 |
+
|
54 |
+
def test_empty_content_validation(self):
|
55 |
+
"""Test validation with empty content"""
|
56 |
+
with pytest.raises(ValueError, match="Audio content cannot be empty"):
|
57 |
+
AudioUploadDto(
|
58 |
+
filename="test.wav",
|
59 |
+
content=b"",
|
60 |
+
content_type="audio/wav"
|
61 |
+
)
|
62 |
+
|
63 |
+
def test_empty_content_type_validation(self):
|
64 |
+
"""Test validation with empty content type"""
|
65 |
+
with pytest.raises(ValueError, match="Content type cannot be empty"):
|
66 |
+
AudioUploadDto(
|
67 |
+
filename="test.wav",
|
68 |
+
content=b"fake_audio_content_" + b"x" * 1000,
|
69 |
+
content_type=""
|
70 |
+
)
|
71 |
+
|
72 |
+
def test_unsupported_file_extension_validation(self):
|
73 |
+
"""Test validation with unsupported file extension"""
|
74 |
+
with pytest.raises(ValueError, match="Unsupported file extension"):
|
75 |
+
AudioUploadDto(
|
76 |
+
filename="test.xyz",
|
77 |
+
content=b"fake_audio_content_" + b"x" * 1000,
|
78 |
+
content_type="audio/wav"
|
79 |
+
)
|
80 |
+
|
81 |
+
def test_supported_file_extensions(self):
|
82 |
+
"""Test all supported file extensions"""
|
83 |
+
supported_extensions = ['.wav', '.mp3', '.m4a', '.flac', '.ogg']
|
84 |
+
content = b"fake_audio_content_" + b"x" * 1000
|
85 |
+
|
86 |
+
for ext in supported_extensions:
|
87 |
+
filename = f"test{ext}"
|
88 |
+
content_type = f"audio/{ext[1:]}" if ext != '.m4a' else "audio/mp4"
|
89 |
+
|
90 |
+
# Should not raise exception
|
91 |
+
dto = AudioUploadDto(
|
92 |
+
filename=filename,
|
93 |
+
content=content,
|
94 |
+
content_type=content_type
|
95 |
+
)
|
96 |
+
assert dto.file_extension == ext
|
97 |
+
|
98 |
+
def test_case_insensitive_extension_validation(self):
|
99 |
+
"""Test case insensitive extension validation"""
|
100 |
+
content = b"fake_audio_content_" + b"x" * 1000
|
101 |
+
|
102 |
+
# Should work with uppercase extension
|
103 |
+
dto = AudioUploadDto(
|
104 |
+
filename="test.WAV",
|
105 |
+
content=content,
|
106 |
+
content_type="audio/wav"
|
107 |
+
)
|
108 |
+
assert dto.file_extension == ".wav" # Should be normalized to lowercase
|
109 |
+
|
110 |
+
def test_file_too_large_validation(self):
|
111 |
+
"""Test validation with file too large"""
|
112 |
+
max_size = 100 * 1024 * 1024 # 100MB
|
113 |
+
large_content = b"x" * (max_size + 1)
|
114 |
+
|
115 |
+
with pytest.raises(ValueError, match="File too large"):
|
116 |
+
AudioUploadDto(
|
117 |
+
filename="test.wav",
|
118 |
+
content=large_content,
|
119 |
+
content_type="audio/wav"
|
120 |
+
)
|
121 |
+
|
122 |
+
def test_file_too_small_validation(self):
|
123 |
+
"""Test validation with file too small"""
|
124 |
+
small_content = b"x" * 500 # Less than 1KB
|
125 |
+
|
126 |
+
with pytest.raises(ValueError, match="File too small"):
|
127 |
+
AudioUploadDto(
|
128 |
+
filename="test.wav",
|
129 |
+
content=small_content,
|
130 |
+
content_type="audio/wav"
|
131 |
+
)
|
132 |
+
|
133 |
+
def test_invalid_content_type_validation(self):
|
134 |
+
"""Test validation with invalid content type"""
|
135 |
+
content = b"fake_audio_content_" + b"x" * 1000
|
136 |
+
|
137 |
+
with pytest.raises(ValueError, match="Invalid content type"):
|
138 |
+
AudioUploadDto(
|
139 |
+
filename="test.wav",
|
140 |
+
content=content,
|
141 |
+
content_type="text/plain" # Not audio/*
|
142 |
+
)
|
143 |
+
|
144 |
+
def test_file_extension_property(self):
|
145 |
+
"""Test file_extension property"""
|
146 |
+
dto = AudioUploadDto(
|
147 |
+
filename="test_audio.MP3",
|
148 |
+
content=b"fake_audio_content_" + b"x" * 1000,
|
149 |
+
content_type="audio/mpeg"
|
150 |
+
)
|
151 |
+
|
152 |
+
assert dto.file_extension == ".mp3"
|
153 |
+
|
154 |
+
def test_base_filename_property(self):
|
155 |
+
"""Test base_filename property"""
|
156 |
+
dto = AudioUploadDto(
|
157 |
+
filename="test_audio.wav",
|
158 |
+
content=b"fake_audio_content_" + b"x" * 1000,
|
159 |
+
content_type="audio/wav"
|
160 |
+
)
|
161 |
+
|
162 |
+
assert dto.base_filename == "test_audio"
|
163 |
+
|
164 |
+
def test_to_dict_method(self):
|
165 |
+
"""Test to_dict method"""
|
166 |
+
filename = "test_audio.wav"
|
167 |
+
content = b"fake_audio_content_" + b"x" * 1000
|
168 |
+
content_type = "audio/wav"
|
169 |
+
|
170 |
+
dto = AudioUploadDto(
|
171 |
+
filename=filename,
|
172 |
+
content=content,
|
173 |
+
content_type=content_type
|
174 |
+
)
|
175 |
+
|
176 |
+
result = dto.to_dict()
|
177 |
+
|
178 |
+
assert result['filename'] == filename
|
179 |
+
assert result['content_type'] == content_type
|
180 |
+
assert result['size'] == len(content)
|
181 |
+
assert result['file_extension'] == ".wav"
|
182 |
+
|
183 |
+
# Should not include content in dict representation
|
184 |
+
assert 'content' not in result
|
185 |
+
|
186 |
+
def test_size_calculation_on_init(self):
|
187 |
+
"""Test that size is calculated automatically if not provided"""
|
188 |
+
content = b"fake_audio_content_" + b"x" * 1500
|
189 |
+
|
190 |
+
dto = AudioUploadDto(
|
191 |
+
filename="test.wav",
|
192 |
+
content=content,
|
193 |
+
content_type="audio/wav"
|
194 |
+
)
|
195 |
+
|
196 |
+
assert dto.size == len(content)
|
197 |
+
|
198 |
+
def test_validation_called_on_init(self):
|
199 |
+
"""Test that validation is called during initialization"""
|
200 |
+
# This should trigger validation and raise an error
|
201 |
+
with pytest.raises(ValueError):
|
202 |
+
AudioUploadDto(
|
203 |
+
filename="", # Invalid empty filename
|
204 |
+
content=b"fake_audio_content_" + b"x" * 1000,
|
205 |
+
content_type="audio/wav"
|
206 |
+
)
|
207 |
+
|
208 |
+
def test_edge_case_minimum_valid_size(self):
|
209 |
+
"""Test edge case with minimum valid file size"""
|
210 |
+
min_content = b"x" * 1024 # Exactly 1KB
|
211 |
+
|
212 |
+
dto = AudioUploadDto(
|
213 |
+
filename="test.wav",
|
214 |
+
content=min_content,
|
215 |
+
content_type="audio/wav"
|
216 |
+
)
|
217 |
+
|
218 |
+
assert dto.size == 1024
|
219 |
+
|
220 |
+
def test_edge_case_maximum_valid_size(self):
|
221 |
+
"""Test edge case with maximum valid file size"""
|
222 |
+
max_size = 100 * 1024 * 1024 # Exactly 100MB
|
223 |
+
max_content = b"x" * max_size
|
224 |
+
|
225 |
+
dto = AudioUploadDto(
|
226 |
+
filename="test.wav",
|
227 |
+
content=max_content,
|
228 |
+
content_type="audio/wav"
|
229 |
+
)
|
230 |
+
|
231 |
+
assert dto.size == max_size
|
232 |
+
|
233 |
+
def test_content_type_mismatch_handling(self):
|
234 |
+
"""Test handling of content type mismatch with filename"""
|
235 |
+
content = b"fake_audio_content_" + b"x" * 1000
|
236 |
+
|
237 |
+
# This should still work as long as content_type starts with 'audio/'
|
238 |
+
dto = AudioUploadDto(
|
239 |
+
filename="test.wav",
|
240 |
+
content=content,
|
241 |
+
content_type="audio/mpeg" # Different from .wav extension
|
242 |
+
)
|
243 |
+
|
244 |
+
assert dto.content_type == "audio/mpeg"
|
245 |
+
assert dto.file_extension == ".wav"
|
tests/unit/application/dtos/test_dto_validation.py
ADDED
@@ -0,0 +1,319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Unit tests for DTO validation utilities"""
|
2 |
+
|
3 |
+
import pytest
|
4 |
+
from unittest.mock import Mock
|
5 |
+
|
6 |
+
from src.application.dtos.dto_validation import (
|
7 |
+
ValidationError,
|
8 |
+
validate_dto,
|
9 |
+
validation_required,
|
10 |
+
validate_field,
|
11 |
+
validate_required,
|
12 |
+
validate_type,
|
13 |
+
validate_range,
|
14 |
+
validate_choices
|
15 |
+
)
|
16 |
+
|
17 |
+
|
18 |
+
class TestValidationError:
|
19 |
+
"""Test cases for ValidationError"""
|
20 |
+
|
21 |
+
def test_validation_error_basic(self):
|
22 |
+
"""Test basic ValidationError creation"""
|
23 |
+
error = ValidationError("Test error message")
|
24 |
+
|
25 |
+
assert str(error) == "Validation error: Test error message"
|
26 |
+
assert error.message == "Test error message"
|
27 |
+
assert error.field is None
|
28 |
+
assert error.value is None
|
29 |
+
|
30 |
+
def test_validation_error_with_field(self):
|
31 |
+
"""Test ValidationError with field information"""
|
32 |
+
error = ValidationError("Invalid value", field="test_field", value="invalid_value")
|
33 |
+
|
34 |
+
assert str(error) == "Validation error for field 'test_field': Invalid value"
|
35 |
+
assert error.message == "Invalid value"
|
36 |
+
assert error.field == "test_field"
|
37 |
+
assert error.value == "invalid_value"
|
38 |
+
|
39 |
+
def test_validation_error_without_field_but_with_value(self):
|
40 |
+
"""Test ValidationError without field but with value"""
|
41 |
+
error = ValidationError("Invalid value", value="test_value")
|
42 |
+
|
43 |
+
assert str(error) == "Validation error: Invalid value"
|
44 |
+
assert error.message == "Invalid value"
|
45 |
+
assert error.field is None
|
46 |
+
assert error.value == "test_value"
|
47 |
+
|
48 |
+
|
49 |
+
class TestValidateDto:
|
50 |
+
"""Test cases for validate_dto function"""
|
51 |
+
|
52 |
+
def test_validate_dto_success_with_validate_method(self):
|
53 |
+
"""Test successful DTO validation with _validate method"""
|
54 |
+
mock_dto = Mock()
|
55 |
+
mock_dto._validate = Mock()
|
56 |
+
|
57 |
+
result = validate_dto(mock_dto)
|
58 |
+
|
59 |
+
assert result is True
|
60 |
+
mock_dto._validate.assert_called_once()
|
61 |
+
|
62 |
+
def test_validate_dto_success_without_validate_method(self):
|
63 |
+
"""Test successful DTO validation without _validate method"""
|
64 |
+
mock_dto = Mock()
|
65 |
+
del mock_dto._validate # Remove the _validate method
|
66 |
+
|
67 |
+
result = validate_dto(mock_dto)
|
68 |
+
|
69 |
+
assert result is True
|
70 |
+
|
71 |
+
def test_validate_dto_failure_value_error(self):
|
72 |
+
"""Test DTO validation failure with ValueError"""
|
73 |
+
mock_dto = Mock()
|
74 |
+
mock_dto._validate.side_effect = ValueError("Validation failed")
|
75 |
+
|
76 |
+
with pytest.raises(ValidationError, match="Validation failed"):
|
77 |
+
validate_dto(mock_dto)
|
78 |
+
|
79 |
+
def test_validate_dto_failure_unexpected_error(self):
|
80 |
+
"""Test DTO validation failure with unexpected error"""
|
81 |
+
mock_dto = Mock()
|
82 |
+
mock_dto._validate.side_effect = RuntimeError("Unexpected error")
|
83 |
+
|
84 |
+
with pytest.raises(ValidationError, match="Validation failed: Unexpected error"):
|
85 |
+
validate_dto(mock_dto)
|
86 |
+
|
87 |
+
|
88 |
+
class TestValidationRequired:
|
89 |
+
"""Test cases for validation_required decorator"""
|
90 |
+
|
91 |
+
def test_validation_required_success(self):
|
92 |
+
"""Test validation_required decorator with successful validation"""
|
93 |
+
mock_instance = Mock()
|
94 |
+
mock_instance._validate = Mock()
|
95 |
+
|
96 |
+
@validation_required
|
97 |
+
def test_method(self):
|
98 |
+
return "success"
|
99 |
+
|
100 |
+
result = test_method(mock_instance)
|
101 |
+
|
102 |
+
assert result == "success"
|
103 |
+
mock_instance._validate.assert_called_once()
|
104 |
+
|
105 |
+
def test_validation_required_validation_error(self):
|
106 |
+
"""Test validation_required decorator with validation error"""
|
107 |
+
mock_instance = Mock()
|
108 |
+
mock_instance._validate.side_effect = ValueError("Validation failed")
|
109 |
+
|
110 |
+
@validation_required
|
111 |
+
def test_method(self):
|
112 |
+
return "success"
|
113 |
+
|
114 |
+
with pytest.raises(ValidationError, match="Validation failed"):
|
115 |
+
test_method(mock_instance)
|
116 |
+
|
117 |
+
def test_validation_required_method_error(self):
|
118 |
+
"""Test validation_required decorator with method execution error"""
|
119 |
+
mock_instance = Mock()
|
120 |
+
mock_instance._validate = Mock()
|
121 |
+
|
122 |
+
@validation_required
|
123 |
+
def test_method(self):
|
124 |
+
raise RuntimeError("Method error")
|
125 |
+
|
126 |
+
with pytest.raises(ValidationError, match="Error in test_method: Method error"):
|
127 |
+
test_method(mock_instance)
|
128 |
+
|
129 |
+
def test_validation_required_preserves_function_metadata(self):
|
130 |
+
"""Test that validation_required preserves function metadata"""
|
131 |
+
@validation_required
|
132 |
+
def test_method(self):
|
133 |
+
"""Test method docstring"""
|
134 |
+
return "success"
|
135 |
+
|
136 |
+
assert test_method.__name__ == "test_method"
|
137 |
+
assert test_method.__doc__ == "Test method docstring"
|
138 |
+
|
139 |
+
|
140 |
+
class TestValidateField:
|
141 |
+
"""Test cases for validate_field function"""
|
142 |
+
|
143 |
+
def test_validate_field_success(self):
|
144 |
+
"""Test successful field validation"""
|
145 |
+
def is_positive(value):
|
146 |
+
return value > 0
|
147 |
+
|
148 |
+
result = validate_field(5, "test_field", is_positive)
|
149 |
+
|
150 |
+
assert result == 5
|
151 |
+
|
152 |
+
def test_validate_field_failure_with_custom_message(self):
|
153 |
+
"""Test field validation failure with custom error message"""
|
154 |
+
def is_positive(value):
|
155 |
+
return value > 0
|
156 |
+
|
157 |
+
with pytest.raises(ValidationError, match="Custom error message"):
|
158 |
+
validate_field(-1, "test_field", is_positive, "Custom error message")
|
159 |
+
|
160 |
+
def test_validate_field_failure_with_default_message(self):
|
161 |
+
"""Test field validation failure with default error message"""
|
162 |
+
def is_positive(value):
|
163 |
+
return value > 0
|
164 |
+
|
165 |
+
with pytest.raises(ValidationError, match="Invalid value for field 'test_field'"):
|
166 |
+
validate_field(-1, "test_field", is_positive)
|
167 |
+
|
168 |
+
def test_validate_field_validator_exception(self):
|
169 |
+
"""Test field validation with validator raising exception"""
|
170 |
+
def failing_validator(value):
|
171 |
+
raise RuntimeError("Validator error")
|
172 |
+
|
173 |
+
with pytest.raises(ValidationError, match="Validation error for field 'test_field': Validator error"):
|
174 |
+
validate_field(5, "test_field", failing_validator)
|
175 |
+
|
176 |
+
|
177 |
+
class TestValidateRequired:
|
178 |
+
"""Test cases for validate_required function"""
|
179 |
+
|
180 |
+
def test_validate_required_success_with_value(self):
|
181 |
+
"""Test successful required validation with valid value"""
|
182 |
+
result = validate_required("test_value", "test_field")
|
183 |
+
assert result == "test_value"
|
184 |
+
|
185 |
+
def test_validate_required_success_with_zero(self):
|
186 |
+
"""Test successful required validation with zero (falsy but valid)"""
|
187 |
+
result = validate_required(0, "test_field")
|
188 |
+
assert result == 0
|
189 |
+
|
190 |
+
def test_validate_required_success_with_false(self):
|
191 |
+
"""Test successful required validation with False (falsy but valid)"""
|
192 |
+
result = validate_required(False, "test_field")
|
193 |
+
assert result is False
|
194 |
+
|
195 |
+
def test_validate_required_failure_with_none(self):
|
196 |
+
"""Test required validation failure with None"""
|
197 |
+
with pytest.raises(ValidationError, match="Field 'test_field' is required"):
|
198 |
+
validate_required(None, "test_field")
|
199 |
+
|
200 |
+
def test_validate_required_failure_with_empty_string(self):
|
201 |
+
"""Test required validation failure with empty string"""
|
202 |
+
with pytest.raises(ValidationError, match="Field 'test_field' cannot be empty"):
|
203 |
+
validate_required("", "test_field")
|
204 |
+
|
205 |
+
def test_validate_required_failure_with_empty_list(self):
|
206 |
+
"""Test required validation failure with empty list"""
|
207 |
+
with pytest.raises(ValidationError, match="Field 'test_field' cannot be empty"):
|
208 |
+
validate_required([], "test_field")
|
209 |
+
|
210 |
+
def test_validate_required_failure_with_empty_dict(self):
|
211 |
+
"""Test required validation failure with empty dict"""
|
212 |
+
with pytest.raises(ValidationError, match="Field 'test_field' cannot be empty"):
|
213 |
+
validate_required({}, "test_field")
|
214 |
+
|
215 |
+
|
216 |
+
class TestValidateType:
|
217 |
+
"""Test cases for validate_type function"""
|
218 |
+
|
219 |
+
def test_validate_type_success_single_type(self):
|
220 |
+
"""Test successful type validation with single type"""
|
221 |
+
result = validate_type("test", "test_field", str)
|
222 |
+
assert result == "test"
|
223 |
+
|
224 |
+
def test_validate_type_success_multiple_types(self):
|
225 |
+
"""Test successful type validation with multiple types"""
|
226 |
+
result1 = validate_type("test", "test_field", (str, int))
|
227 |
+
assert result1 == "test"
|
228 |
+
|
229 |
+
result2 = validate_type(123, "test_field", (str, int))
|
230 |
+
assert result2 == 123
|
231 |
+
|
232 |
+
def test_validate_type_failure_single_type(self):
|
233 |
+
"""Test type validation failure with single type"""
|
234 |
+
with pytest.raises(ValidationError, match="Field 'test_field' must be of type str, got int"):
|
235 |
+
validate_type(123, "test_field", str)
|
236 |
+
|
237 |
+
def test_validate_type_failure_multiple_types(self):
|
238 |
+
"""Test type validation failure with multiple types"""
|
239 |
+
with pytest.raises(ValidationError, match="Field 'test_field' must be of type str or int, got float"):
|
240 |
+
validate_type(1.5, "test_field", (str, int))
|
241 |
+
|
242 |
+
|
243 |
+
class TestValidateRange:
|
244 |
+
"""Test cases for validate_range function"""
|
245 |
+
|
246 |
+
def test_validate_range_success_within_range(self):
|
247 |
+
"""Test successful range validation within range"""
|
248 |
+
result = validate_range(5, "test_field", min_value=1, max_value=10)
|
249 |
+
assert result == 5
|
250 |
+
|
251 |
+
def test_validate_range_success_at_boundaries(self):
|
252 |
+
"""Test successful range validation at boundaries"""
|
253 |
+
result1 = validate_range(1, "test_field", min_value=1, max_value=10)
|
254 |
+
assert result1 == 1
|
255 |
+
|
256 |
+
result2 = validate_range(10, "test_field", min_value=1, max_value=10)
|
257 |
+
assert result2 == 10
|
258 |
+
|
259 |
+
def test_validate_range_success_only_min(self):
|
260 |
+
"""Test successful range validation with only minimum"""
|
261 |
+
result = validate_range(5, "test_field", min_value=1)
|
262 |
+
assert result == 5
|
263 |
+
|
264 |
+
def test_validate_range_success_only_max(self):
|
265 |
+
"""Test successful range validation with only maximum"""
|
266 |
+
result = validate_range(5, "test_field", max_value=10)
|
267 |
+
assert result == 5
|
268 |
+
|
269 |
+
def test_validate_range_success_no_limits(self):
|
270 |
+
"""Test successful range validation with no limits"""
|
271 |
+
result = validate_range(5, "test_field")
|
272 |
+
assert result == 5
|
273 |
+
|
274 |
+
def test_validate_range_failure_below_minimum(self):
|
275 |
+
"""Test range validation failure below minimum"""
|
276 |
+
with pytest.raises(ValidationError, match="Field 'test_field' must be >= 1, got 0"):
|
277 |
+
validate_range(0, "test_field", min_value=1, max_value=10)
|
278 |
+
|
279 |
+
def test_validate_range_failure_above_maximum(self):
|
280 |
+
"""Test range validation failure above maximum"""
|
281 |
+
with pytest.raises(ValidationError, match="Field 'test_field' must be <= 10, got 11"):
|
282 |
+
validate_range(11, "test_field", min_value=1, max_value=10)
|
283 |
+
|
284 |
+
def test_validate_range_with_float_values(self):
|
285 |
+
"""Test range validation with float values"""
|
286 |
+
result = validate_range(1.5, "test_field", min_value=1.0, max_value=2.0)
|
287 |
+
assert result == 1.5
|
288 |
+
|
289 |
+
|
290 |
+
class TestValidateChoices:
|
291 |
+
"""Test cases for validate_choices function"""
|
292 |
+
|
293 |
+
def test_validate_choices_success(self):
|
294 |
+
"""Test successful choices validation"""
|
295 |
+
choices = ["option1", "option2", "option3"]
|
296 |
+
result = validate_choices("option2", "test_field", choices)
|
297 |
+
assert result == "option2"
|
298 |
+
|
299 |
+
def test_validate_choices_failure(self):
|
300 |
+
"""Test choices validation failure"""
|
301 |
+
choices = ["option1", "option2", "option3"]
|
302 |
+
|
303 |
+
with pytest.raises(ValidationError, match="Field 'test_field' must be one of \\['option1', 'option2', 'option3'\\], got 'invalid'"):
|
304 |
+
validate_choices("invalid", "test_field", choices)
|
305 |
+
|
306 |
+
def test_validate_choices_with_different_types(self):
|
307 |
+
"""Test choices validation with different value types"""
|
308 |
+
choices = [1, 2, 3, "four"]
|
309 |
+
|
310 |
+
result1 = validate_choices(2, "test_field", choices)
|
311 |
+
assert result1 == 2
|
312 |
+
|
313 |
+
result2 = validate_choices("four", "test_field", choices)
|
314 |
+
assert result2 == "four"
|
315 |
+
|
316 |
+
def test_validate_choices_empty_choices(self):
|
317 |
+
"""Test choices validation with empty choices list"""
|
318 |
+
with pytest.raises(ValidationError, match="Field 'test_field' must be one of \\[\\], got 'value'"):
|
319 |
+
validate_choices("value", "test_field", [])
|
tests/unit/application/dtos/test_processing_request_dto.py
ADDED
@@ -0,0 +1,383 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Unit tests for ProcessingRequestDto"""
|
2 |
+
|
3 |
+
import pytest
|
4 |
+
|
5 |
+
from src.application.dtos.processing_request_dto import ProcessingRequestDto
|
6 |
+
from src.application.dtos.audio_upload_dto import AudioUploadDto
|
7 |
+
|
8 |
+
|
9 |
+
class TestProcessingRequestDto:
|
10 |
+
"""Test cases for ProcessingRequestDto"""
|
11 |
+
|
12 |
+
@pytest.fixture
|
13 |
+
def sample_audio_upload(self):
|
14 |
+
"""Create sample audio upload DTO"""
|
15 |
+
return AudioUploadDto(
|
16 |
+
filename="test_audio.wav",
|
17 |
+
content=b"fake_audio_content_" + b"x" * 1000,
|
18 |
+
content_type="audio/wav"
|
19 |
+
)
|
20 |
+
|
21 |
+
def test_valid_processing_request_dto(self, sample_audio_upload):
|
22 |
+
"""Test creating a valid ProcessingRequestDto"""
|
23 |
+
dto = ProcessingRequestDto(
|
24 |
+
audio=sample_audio_upload,
|
25 |
+
asr_model="whisper-small",
|
26 |
+
target_language="es",
|
27 |
+
voice="kokoro",
|
28 |
+
speed=1.0,
|
29 |
+
source_language="en"
|
30 |
+
)
|
31 |
+
|
32 |
+
assert dto.audio == sample_audio_upload
|
33 |
+
assert dto.asr_model == "whisper-small"
|
34 |
+
assert dto.target_language == "es"
|
35 |
+
assert dto.voice == "kokoro"
|
36 |
+
assert dto.speed == 1.0
|
37 |
+
assert dto.source_language == "en"
|
38 |
+
assert dto.additional_params == {}
|
39 |
+
|
40 |
+
def test_processing_request_dto_with_defaults(self, sample_audio_upload):
|
41 |
+
"""Test creating ProcessingRequestDto with default values"""
|
42 |
+
dto = ProcessingRequestDto(
|
43 |
+
audio=sample_audio_upload,
|
44 |
+
asr_model="whisper-medium",
|
45 |
+
target_language="fr",
|
46 |
+
voice="dia"
|
47 |
+
)
|
48 |
+
|
49 |
+
assert dto.speed == 1.0 # Default speed
|
50 |
+
assert dto.source_language is None # Default source language
|
51 |
+
assert dto.additional_params == {} # Default additional params
|
52 |
+
|
53 |
+
def test_processing_request_dto_with_additional_params(self, sample_audio_upload):
|
54 |
+
"""Test creating ProcessingRequestDto with additional parameters"""
|
55 |
+
additional_params = {
|
56 |
+
"custom_param": "value",
|
57 |
+
"another_param": 123
|
58 |
+
}
|
59 |
+
|
60 |
+
dto = ProcessingRequestDto(
|
61 |
+
audio=sample_audio_upload,
|
62 |
+
asr_model="whisper-large",
|
63 |
+
target_language="de",
|
64 |
+
voice="cosyvoice2",
|
65 |
+
additional_params=additional_params
|
66 |
+
)
|
67 |
+
|
68 |
+
assert dto.additional_params == additional_params
|
69 |
+
|
70 |
+
def test_invalid_audio_type_validation(self):
|
71 |
+
"""Test validation with invalid audio type"""
|
72 |
+
with pytest.raises(ValueError, match="Audio must be an AudioUploadDto instance"):
|
73 |
+
ProcessingRequestDto(
|
74 |
+
audio="invalid_audio", # Not AudioUploadDto
|
75 |
+
asr_model="whisper-small",
|
76 |
+
target_language="es",
|
77 |
+
voice="kokoro"
|
78 |
+
)
|
79 |
+
|
80 |
+
def test_empty_asr_model_validation(self, sample_audio_upload):
|
81 |
+
"""Test validation with empty ASR model"""
|
82 |
+
with pytest.raises(ValueError, match="ASR model cannot be empty"):
|
83 |
+
ProcessingRequestDto(
|
84 |
+
audio=sample_audio_upload,
|
85 |
+
asr_model="",
|
86 |
+
target_language="es",
|
87 |
+
voice="kokoro"
|
88 |
+
)
|
89 |
+
|
90 |
+
def test_unsupported_asr_model_validation(self, sample_audio_upload):
|
91 |
+
"""Test validation with unsupported ASR model"""
|
92 |
+
with pytest.raises(ValueError, match="Unsupported ASR model"):
|
93 |
+
ProcessingRequestDto(
|
94 |
+
audio=sample_audio_upload,
|
95 |
+
asr_model="invalid-model",
|
96 |
+
target_language="es",
|
97 |
+
voice="kokoro"
|
98 |
+
)
|
99 |
+
|
100 |
+
def test_supported_asr_models(self, sample_audio_upload):
|
101 |
+
"""Test all supported ASR models"""
|
102 |
+
supported_models = ['whisper-small', 'whisper-medium', 'whisper-large', 'parakeet']
|
103 |
+
|
104 |
+
for model in supported_models:
|
105 |
+
# Should not raise exception
|
106 |
+
dto = ProcessingRequestDto(
|
107 |
+
audio=sample_audio_upload,
|
108 |
+
asr_model=model,
|
109 |
+
target_language="es",
|
110 |
+
voice="kokoro"
|
111 |
+
)
|
112 |
+
assert dto.asr_model == model
|
113 |
+
|
114 |
+
def test_empty_target_language_validation(self, sample_audio_upload):
|
115 |
+
"""Test validation with empty target language"""
|
116 |
+
with pytest.raises(ValueError, match="Target language cannot be empty"):
|
117 |
+
ProcessingRequestDto(
|
118 |
+
audio=sample_audio_upload,
|
119 |
+
asr_model="whisper-small",
|
120 |
+
target_language="",
|
121 |
+
voice="kokoro"
|
122 |
+
)
|
123 |
+
|
124 |
+
def test_unsupported_target_language_validation(self, sample_audio_upload):
|
125 |
+
"""Test validation with unsupported target language"""
|
126 |
+
with pytest.raises(ValueError, match="Unsupported target language"):
|
127 |
+
ProcessingRequestDto(
|
128 |
+
audio=sample_audio_upload,
|
129 |
+
asr_model="whisper-small",
|
130 |
+
target_language="invalid-lang",
|
131 |
+
voice="kokoro"
|
132 |
+
)
|
133 |
+
|
134 |
+
def test_unsupported_source_language_validation(self, sample_audio_upload):
|
135 |
+
"""Test validation with unsupported source language"""
|
136 |
+
with pytest.raises(ValueError, match="Unsupported source language"):
|
137 |
+
ProcessingRequestDto(
|
138 |
+
audio=sample_audio_upload,
|
139 |
+
asr_model="whisper-small",
|
140 |
+
target_language="es",
|
141 |
+
voice="kokoro",
|
142 |
+
source_language="invalid-lang"
|
143 |
+
)
|
144 |
+
|
145 |
+
def test_supported_languages(self, sample_audio_upload):
|
146 |
+
"""Test all supported languages"""
|
147 |
+
supported_languages = [
|
148 |
+
'en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh',
|
149 |
+
'ar', 'hi', 'tr', 'pl', 'nl', 'sv', 'da', 'no', 'fi'
|
150 |
+
]
|
151 |
+
|
152 |
+
for lang in supported_languages:
|
153 |
+
# Should not raise exception
|
154 |
+
dto = ProcessingRequestDto(
|
155 |
+
audio=sample_audio_upload,
|
156 |
+
asr_model="whisper-small",
|
157 |
+
target_language=lang,
|
158 |
+
voice="kokoro",
|
159 |
+
source_language=lang
|
160 |
+
)
|
161 |
+
assert dto.target_language == lang
|
162 |
+
assert dto.source_language == lang
|
163 |
+
|
164 |
+
def test_empty_voice_validation(self, sample_audio_upload):
|
165 |
+
"""Test validation with empty voice"""
|
166 |
+
with pytest.raises(ValueError, match="Voice cannot be empty"):
|
167 |
+
ProcessingRequestDto(
|
168 |
+
audio=sample_audio_upload,
|
169 |
+
asr_model="whisper-small",
|
170 |
+
target_language="es",
|
171 |
+
voice=""
|
172 |
+
)
|
173 |
+
|
174 |
+
def test_unsupported_voice_validation(self, sample_audio_upload):
|
175 |
+
"""Test validation with unsupported voice"""
|
176 |
+
with pytest.raises(ValueError, match="Unsupported voice"):
|
177 |
+
ProcessingRequestDto(
|
178 |
+
audio=sample_audio_upload,
|
179 |
+
asr_model="whisper-small",
|
180 |
+
target_language="es",
|
181 |
+
voice="invalid-voice"
|
182 |
+
)
|
183 |
+
|
184 |
+
def test_supported_voices(self, sample_audio_upload):
|
185 |
+
"""Test all supported voices"""
|
186 |
+
supported_voices = ['kokoro', 'dia', 'cosyvoice2', 'dummy']
|
187 |
+
|
188 |
+
for voice in supported_voices:
|
189 |
+
# Should not raise exception
|
190 |
+
dto = ProcessingRequestDto(
|
191 |
+
audio=sample_audio_upload,
|
192 |
+
asr_model="whisper-small",
|
193 |
+
target_language="es",
|
194 |
+
voice=voice
|
195 |
+
)
|
196 |
+
assert dto.voice == voice
|
197 |
+
|
198 |
+
def test_speed_range_validation_too_low(self, sample_audio_upload):
|
199 |
+
"""Test validation with speed too low"""
|
200 |
+
with pytest.raises(ValueError, match="Speed must be between 0.5 and 2.0"):
|
201 |
+
ProcessingRequestDto(
|
202 |
+
audio=sample_audio_upload,
|
203 |
+
asr_model="whisper-small",
|
204 |
+
target_language="es",
|
205 |
+
voice="kokoro",
|
206 |
+
speed=0.3 # Too low
|
207 |
+
)
|
208 |
+
|
209 |
+
def test_speed_range_validation_too_high(self, sample_audio_upload):
|
210 |
+
"""Test validation with speed too high"""
|
211 |
+
with pytest.raises(ValueError, match="Speed must be between 0.5 and 2.0"):
|
212 |
+
ProcessingRequestDto(
|
213 |
+
audio=sample_audio_upload,
|
214 |
+
asr_model="whisper-small",
|
215 |
+
target_language="es",
|
216 |
+
voice="kokoro",
|
217 |
+
speed=2.5 # Too high
|
218 |
+
)
|
219 |
+
|
220 |
+
def test_valid_speed_range(self, sample_audio_upload):
|
221 |
+
"""Test valid speed range"""
|
222 |
+
valid_speeds = [0.5, 1.0, 1.5, 2.0]
|
223 |
+
|
224 |
+
for speed in valid_speeds:
|
225 |
+
# Should not raise exception
|
226 |
+
dto = ProcessingRequestDto(
|
227 |
+
audio=sample_audio_upload,
|
228 |
+
asr_model="whisper-small",
|
229 |
+
target_language="es",
|
230 |
+
voice="kokoro",
|
231 |
+
speed=speed
|
232 |
+
)
|
233 |
+
assert dto.speed == speed
|
234 |
+
|
235 |
+
def test_invalid_additional_params_type(self, sample_audio_upload):
|
236 |
+
"""Test validation with invalid additional params type"""
|
237 |
+
with pytest.raises(ValueError, match="Additional params must be a dictionary"):
|
238 |
+
ProcessingRequestDto(
|
239 |
+
audio=sample_audio_upload,
|
240 |
+
asr_model="whisper-small",
|
241 |
+
target_language="es",
|
242 |
+
voice="kokoro",
|
243 |
+
additional_params="invalid" # Not a dict
|
244 |
+
)
|
245 |
+
|
246 |
+
def test_requires_translation_property_same_language(self, sample_audio_upload):
|
247 |
+
"""Test requires_translation property when source and target are same"""
|
248 |
+
dto = ProcessingRequestDto(
|
249 |
+
audio=sample_audio_upload,
|
250 |
+
asr_model="whisper-small",
|
251 |
+
target_language="en",
|
252 |
+
voice="kokoro",
|
253 |
+
source_language="en"
|
254 |
+
)
|
255 |
+
|
256 |
+
assert dto.requires_translation is False
|
257 |
+
|
258 |
+
def test_requires_translation_property_different_languages(self, sample_audio_upload):
|
259 |
+
"""Test requires_translation property when source and target are different"""
|
260 |
+
dto = ProcessingRequestDto(
|
261 |
+
audio=sample_audio_upload,
|
262 |
+
asr_model="whisper-small",
|
263 |
+
target_language="es",
|
264 |
+
voice="kokoro",
|
265 |
+
source_language="en"
|
266 |
+
)
|
267 |
+
|
268 |
+
assert dto.requires_translation is True
|
269 |
+
|
270 |
+
def test_requires_translation_property_no_source(self, sample_audio_upload):
|
271 |
+
"""Test requires_translation property when no source language specified"""
|
272 |
+
dto = ProcessingRequestDto(
|
273 |
+
audio=sample_audio_upload,
|
274 |
+
asr_model="whisper-small",
|
275 |
+
target_language="es",
|
276 |
+
voice="kokoro"
|
277 |
+
)
|
278 |
+
|
279 |
+
assert dto.requires_translation is True # Assume translation needed
|
280 |
+
|
281 |
+
def test_to_dict_method(self, sample_audio_upload):
|
282 |
+
"""Test to_dict method"""
|
283 |
+
dto = ProcessingRequestDto(
|
284 |
+
audio=sample_audio_upload,
|
285 |
+
asr_model="whisper-small",
|
286 |
+
target_language="es",
|
287 |
+
voice="kokoro",
|
288 |
+
speed=1.5,
|
289 |
+
source_language="en",
|
290 |
+
additional_params={"custom": "value"}
|
291 |
+
)
|
292 |
+
|
293 |
+
result = dto.to_dict()
|
294 |
+
|
295 |
+
assert result['audio'] == sample_audio_upload.to_dict()
|
296 |
+
assert result['asr_model'] == "whisper-small"
|
297 |
+
assert result['target_language'] == "es"
|
298 |
+
assert result['source_language'] == "en"
|
299 |
+
assert result['voice'] == "kokoro"
|
300 |
+
assert result['speed'] == 1.5
|
301 |
+
assert result['requires_translation'] is True
|
302 |
+
assert result['additional_params'] == {"custom": "value"}
|
303 |
+
|
304 |
+
def test_from_dict_method(self, sample_audio_upload):
|
305 |
+
"""Test from_dict class method"""
|
306 |
+
data = {
|
307 |
+
'audio': sample_audio_upload,
|
308 |
+
'asr_model': 'whisper-medium',
|
309 |
+
'target_language': 'fr',
|
310 |
+
'voice': 'dia',
|
311 |
+
'speed': 1.2,
|
312 |
+
'source_language': 'en',
|
313 |
+
'additional_params': {'test': 'value'}
|
314 |
+
}
|
315 |
+
|
316 |
+
dto = ProcessingRequestDto.from_dict(data)
|
317 |
+
|
318 |
+
assert dto.audio == sample_audio_upload
|
319 |
+
assert dto.asr_model == 'whisper-medium'
|
320 |
+
assert dto.target_language == 'fr'
|
321 |
+
assert dto.voice == 'dia'
|
322 |
+
assert dto.speed == 1.2
|
323 |
+
assert dto.source_language == 'en'
|
324 |
+
assert dto.additional_params == {'test': 'value'}
|
325 |
+
|
326 |
+
def test_from_dict_method_with_audio_dict(self):
|
327 |
+
"""Test from_dict method with audio as dictionary"""
|
328 |
+
audio_data = {
|
329 |
+
'filename': 'test.wav',
|
330 |
+
'content': b'fake_content' + b'x' * 1000,
|
331 |
+
'content_type': 'audio/wav'
|
332 |
+
}
|
333 |
+
|
334 |
+
data = {
|
335 |
+
'audio': audio_data,
|
336 |
+
'asr_model': 'whisper-small',
|
337 |
+
'target_language': 'es',
|
338 |
+
'voice': 'kokoro'
|
339 |
+
}
|
340 |
+
|
341 |
+
dto = ProcessingRequestDto.from_dict(data)
|
342 |
+
|
343 |
+
assert isinstance(dto.audio, AudioUploadDto)
|
344 |
+
assert dto.audio.filename == 'test.wav'
|
345 |
+
assert dto.audio.content_type == 'audio/wav'
|
346 |
+
|
347 |
+
def test_from_dict_method_with_defaults(self, sample_audio_upload):
|
348 |
+
"""Test from_dict method with default values"""
|
349 |
+
data = {
|
350 |
+
'audio': sample_audio_upload,
|
351 |
+
'asr_model': 'whisper-small',
|
352 |
+
'target_language': 'es',
|
353 |
+
'voice': 'kokoro'
|
354 |
+
}
|
355 |
+
|
356 |
+
dto = ProcessingRequestDto.from_dict(data)
|
357 |
+
|
358 |
+
assert dto.speed == 1.0 # Default
|
359 |
+
assert dto.source_language is None # Default
|
360 |
+
assert dto.additional_params is None # Default
|
361 |
+
|
362 |
+
def test_validation_called_on_init(self, sample_audio_upload):
|
363 |
+
"""Test that validation is called during initialization"""
|
364 |
+
# This should trigger validation and raise an error
|
365 |
+
with pytest.raises(ValueError):
|
366 |
+
ProcessingRequestDto(
|
367 |
+
audio=sample_audio_upload,
|
368 |
+
asr_model="", # Invalid empty model
|
369 |
+
target_language="es",
|
370 |
+
voice="kokoro"
|
371 |
+
)
|
372 |
+
|
373 |
+
def test_additional_params_default_initialization(self, sample_audio_upload):
|
374 |
+
"""Test that additional_params is initialized to empty dict if None"""
|
375 |
+
dto = ProcessingRequestDto(
|
376 |
+
audio=sample_audio_upload,
|
377 |
+
asr_model="whisper-small",
|
378 |
+
target_language="es",
|
379 |
+
voice="kokoro",
|
380 |
+
additional_params=None
|
381 |
+
)
|
382 |
+
|
383 |
+
assert dto.additional_params == {}
|
tests/unit/application/dtos/test_processing_result_dto.py
ADDED
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Unit tests for ProcessingResultDto"""
|
2 |
+
|
3 |
+
import pytest
|
4 |
+
from datetime import datetime
|
5 |
+
|
6 |
+
from src.application.dtos.processing_result_dto import ProcessingResultDto
|
7 |
+
|
8 |
+
|
9 |
+
class TestProcessingResultDto:
|
10 |
+
"""Test cases for ProcessingResultDto"""
|
11 |
+
|
12 |
+
def test_valid_success_processing_result_dto(self):
|
13 |
+
"""Test creating a valid successful ProcessingResultDto"""
|
14 |
+
dto = ProcessingResultDto(
|
15 |
+
success=True,
|
16 |
+
original_text="Hello world",
|
17 |
+
translated_text="Hola mundo",
|
18 |
+
audio_path="/tmp/output.wav",
|
19 |
+
processing_time=5.2,
|
20 |
+
metadata={"correlation_id": "test-123"}
|
21 |
+
)
|
22 |
+
|
23 |
+
assert dto.success is True
|
24 |
+
assert dto.original_text == "Hello world"
|
25 |
+
assert dto.translated_text == "Hola mundo"
|
26 |
+
assert dto.audio_path == "/tmp/output.wav"
|
27 |
+
assert dto.processing_time == 5.2
|
28 |
+
assert dto.metadata == {"correlation_id": "test-123"}
|
29 |
+
assert dto.error_message is None
|
30 |
+
assert dto.error_code is None
|
31 |
+
assert isinstance(dto.timestamp, datetime)
|
32 |
+
|
33 |
+
def test_valid_error_processing_result_dto(self):
|
34 |
+
"""Test creating a valid error ProcessingResultDto"""
|
35 |
+
dto = ProcessingResultDto(
|
36 |
+
success=False,
|
37 |
+
error_message="Processing failed",
|
38 |
+
error_code="STT_ERROR",
|
39 |
+
processing_time=2.1,
|
40 |
+
metadata={"correlation_id": "test-456"}
|
41 |
+
)
|
42 |
+
|
43 |
+
assert dto.success is False
|
44 |
+
assert dto.error_message == "Processing failed"
|
45 |
+
assert dto.error_code == "STT_ERROR"
|
46 |
+
assert dto.processing_time == 2.1
|
47 |
+
assert dto.metadata == {"correlation_id": "test-456"}
|
48 |
+
assert dto.original_text is None
|
49 |
+
assert dto.translated_text is None
|
50 |
+
assert dto.audio_path is None
|
51 |
+
assert isinstance(dto.timestamp, datetime)
|
52 |
+
|
53 |
+
def test_processing_result_dto_with_defaults(self):
|
54 |
+
"""Test creating ProcessingResultDto with default values"""
|
55 |
+
dto = ProcessingResultDto(success=True, original_text="Test")
|
56 |
+
|
57 |
+
assert dto.success is True
|
58 |
+
assert dto.original_text == "Test"
|
59 |
+
assert dto.translated_text is None
|
60 |
+
assert dto.audio_path is None
|
61 |
+
assert dto.processing_time == 0.0
|
62 |
+
assert dto.error_message is None
|
63 |
+
assert dto.error_code is None
|
64 |
+
assert dto.metadata == {}
|
65 |
+
assert isinstance(dto.timestamp, datetime)
|
66 |
+
|
67 |
+
def test_processing_result_dto_with_explicit_timestamp(self):
|
68 |
+
"""Test creating ProcessingResultDto with explicit timestamp"""
|
69 |
+
test_timestamp = datetime(2023, 1, 1, 12, 0, 0)
|
70 |
+
|
71 |
+
dto = ProcessingResultDto(
|
72 |
+
success=True,
|
73 |
+
original_text="Test",
|
74 |
+
timestamp=test_timestamp
|
75 |
+
)
|
76 |
+
|
77 |
+
assert dto.timestamp == test_timestamp
|
78 |
+
|
79 |
+
def test_invalid_success_type_validation(self):
|
80 |
+
"""Test validation with invalid success type"""
|
81 |
+
with pytest.raises(ValueError, match="Success must be a boolean value"):
|
82 |
+
ProcessingResultDto(
|
83 |
+
success="true", # String instead of boolean
|
84 |
+
original_text="Test"
|
85 |
+
)
|
86 |
+
|
87 |
+
def test_negative_processing_time_validation(self):
|
88 |
+
"""Test validation with negative processing time"""
|
89 |
+
with pytest.raises(ValueError, match="Processing time cannot be negative"):
|
90 |
+
ProcessingResultDto(
|
91 |
+
success=True,
|
92 |
+
original_text="Test",
|
93 |
+
processing_time=-1.0
|
94 |
+
)
|
95 |
+
|
96 |
+
def test_successful_processing_without_output_validation(self):
|
97 |
+
"""Test validation for successful processing without any output"""
|
98 |
+
with pytest.raises(ValueError, match="Successful processing must have at least one output"):
|
99 |
+
ProcessingResultDto(
|
100 |
+
success=True,
|
101 |
+
# No original_text, translated_text, or audio_path
|
102 |
+
)
|
103 |
+
|
104 |
+
def test_failed_processing_without_error_message_validation(self):
|
105 |
+
"""Test validation for failed processing without error message"""
|
106 |
+
with pytest.raises(ValueError, match="Failed processing must include an error message"):
|
107 |
+
ProcessingResultDto(
|
108 |
+
success=False,
|
109 |
+
# No error_message
|
110 |
+
)
|
111 |
+
|
112 |
+
def test_invalid_error_code_validation(self):
|
113 |
+
"""Test validation with invalid error code"""
|
114 |
+
with pytest.raises(ValueError, match="Invalid error code"):
|
115 |
+
ProcessingResultDto(
|
116 |
+
success=False,
|
117 |
+
error_message="Test error",
|
118 |
+
error_code="INVALID_CODE"
|
119 |
+
)
|
120 |
+
|
121 |
+
def test_valid_error_codes(self):
|
122 |
+
"""Test all valid error codes"""
|
123 |
+
valid_error_codes = [
|
124 |
+
'STT_ERROR', 'TRANSLATION_ERROR', 'TTS_ERROR',
|
125 |
+
'AUDIO_FORMAT_ERROR', 'VALIDATION_ERROR', 'SYSTEM_ERROR'
|
126 |
+
]
|
127 |
+
|
128 |
+
for error_code in valid_error_codes:
|
129 |
+
# Should not raise exception
|
130 |
+
dto = ProcessingResultDto(
|
131 |
+
success=False,
|
132 |
+
error_message="Test error",
|
133 |
+
error_code=error_code
|
134 |
+
)
|
135 |
+
assert dto.error_code == error_code
|
136 |
+
|
137 |
+
def test_invalid_metadata_type_validation(self):
|
138 |
+
"""Test validation with invalid metadata type"""
|
139 |
+
with pytest.raises(ValueError, match="Metadata must be a dictionary"):
|
140 |
+
ProcessingResultDto(
|
141 |
+
success=True,
|
142 |
+
original_text="Test",
|
143 |
+
metadata="invalid" # String instead of dict
|
144 |
+
)
|
145 |
+
|
146 |
+
def test_has_text_output_property(self):
|
147 |
+
"""Test has_text_output property"""
|
148 |
+
# With original text only
|
149 |
+
dto1 = ProcessingResultDto(success=True, original_text="Test")
|
150 |
+
assert dto1.has_text_output is True
|
151 |
+
|
152 |
+
# With translated text only
|
153 |
+
dto2 = ProcessingResultDto(success=True, translated_text="Prueba")
|
154 |
+
assert dto2.has_text_output is True
|
155 |
+
|
156 |
+
# With both texts
|
157 |
+
dto3 = ProcessingResultDto(success=True, original_text="Test", translated_text="Prueba")
|
158 |
+
assert dto3.has_text_output is True
|
159 |
+
|
160 |
+
# With no text
|
161 |
+
dto4 = ProcessingResultDto(success=True, audio_path="/tmp/test.wav")
|
162 |
+
assert dto4.has_text_output is False
|
163 |
+
|
164 |
+
def test_has_audio_output_property(self):
|
165 |
+
"""Test has_audio_output property"""
|
166 |
+
# With audio path
|
167 |
+
dto1 = ProcessingResultDto(success=True, audio_path="/tmp/test.wav")
|
168 |
+
assert dto1.has_audio_output is True
|
169 |
+
|
170 |
+
# Without audio path
|
171 |
+
dto2 = ProcessingResultDto(success=True, original_text="Test")
|
172 |
+
assert dto2.has_audio_output is False
|
173 |
+
|
174 |
+
def test_is_complete_property(self):
|
175 |
+
"""Test is_complete property"""
|
176 |
+
# Successful processing
|
177 |
+
dto1 = ProcessingResultDto(success=True, original_text="Test")
|
178 |
+
assert dto1.is_complete is True
|
179 |
+
|
180 |
+
# Failed processing with error message
|
181 |
+
dto2 = ProcessingResultDto(success=False, error_message="Error")
|
182 |
+
assert dto2.is_complete is True
|
183 |
+
|
184 |
+
# This shouldn't happen due to validation, but test the logic
|
185 |
+
dto3 = ProcessingResultDto.__new__(ProcessingResultDto)
|
186 |
+
dto3.success = False
|
187 |
+
dto3.error_message = None
|
188 |
+
assert dto3.is_complete is False
|
189 |
+
|
190 |
+
def test_add_metadata_method(self):
|
191 |
+
"""Test add_metadata method"""
|
192 |
+
dto = ProcessingResultDto(success=True, original_text="Test")
|
193 |
+
|
194 |
+
dto.add_metadata("key1", "value1")
|
195 |
+
dto.add_metadata("key2", 123)
|
196 |
+
|
197 |
+
assert dto.metadata["key1"] == "value1"
|
198 |
+
assert dto.metadata["key2"] == 123
|
199 |
+
|
200 |
+
def test_add_metadata_method_with_none_metadata(self):
|
201 |
+
"""Test add_metadata method when metadata is None"""
|
202 |
+
dto = ProcessingResultDto.__new__(ProcessingResultDto)
|
203 |
+
dto.success = True
|
204 |
+
dto.original_text = "Test"
|
205 |
+
dto.metadata = None
|
206 |
+
|
207 |
+
dto.add_metadata("key", "value")
|
208 |
+
|
209 |
+
assert dto.metadata == {"key": "value"}
|
210 |
+
|
211 |
+
def test_get_metadata_method(self):
|
212 |
+
"""Test get_metadata method"""
|
213 |
+
dto = ProcessingResultDto(
|
214 |
+
success=True,
|
215 |
+
original_text="Test",
|
216 |
+
metadata={"existing_key": "existing_value"}
|
217 |
+
)
|
218 |
+
|
219 |
+
# Get existing key
|
220 |
+
assert dto.get_metadata("existing_key") == "existing_value"
|
221 |
+
|
222 |
+
# Get non-existing key with default
|
223 |
+
assert dto.get_metadata("non_existing", "default") == "default"
|
224 |
+
|
225 |
+
# Get non-existing key without default
|
226 |
+
assert dto.get_metadata("non_existing") is None
|
227 |
+
|
228 |
+
def test_get_metadata_method_with_none_metadata(self):
|
229 |
+
"""Test get_metadata method when metadata is None"""
|
230 |
+
dto = ProcessingResultDto.__new__(ProcessingResultDto)
|
231 |
+
dto.success = True
|
232 |
+
dto.original_text = "Test"
|
233 |
+
dto.metadata = None
|
234 |
+
|
235 |
+
assert dto.get_metadata("key", "default") == "default"
|
236 |
+
assert dto.get_metadata("key") is None
|
237 |
+
|
238 |
+
def test_to_dict_method(self):
|
239 |
+
"""Test to_dict method"""
|
240 |
+
test_timestamp = datetime(2023, 1, 1, 12, 0, 0)
|
241 |
+
|
242 |
+
dto = ProcessingResultDto(
|
243 |
+
success=True,
|
244 |
+
original_text="Hello world",
|
245 |
+
translated_text="Hola mundo",
|
246 |
+
audio_path="/tmp/output.wav",
|
247 |
+
processing_time=5.2,
|
248 |
+
error_message=None,
|
249 |
+
error_code=None,
|
250 |
+
metadata={"correlation_id": "test-123"},
|
251 |
+
timestamp=test_timestamp
|
252 |
+
)
|
253 |
+
|
254 |
+
result = dto.to_dict()
|
255 |
+
|
256 |
+
assert result['success'] is True
|
257 |
+
assert result['original_text'] == "Hello world"
|
258 |
+
assert result['translated_text'] == "Hola mundo"
|
259 |
+
assert result['audio_path'] == "/tmp/output.wav"
|
260 |
+
assert result['processing_time'] == 5.2
|
261 |
+
assert result['error_message'] is None
|
262 |
+
assert result['error_code'] is None
|
263 |
+
assert result['metadata'] == {"correlation_id": "test-123"}
|
264 |
+
assert result['timestamp'] == test_timestamp.isoformat()
|
265 |
+
assert result['has_text_output'] is True
|
266 |
+
assert result['has_audio_output'] is True
|
267 |
+
assert result['is_complete'] is True
|
268 |
+
|
269 |
+
def test_to_dict_method_with_none_timestamp(self):
|
270 |
+
"""Test to_dict method with None timestamp"""
|
271 |
+
dto = ProcessingResultDto.__new__(ProcessingResultDto)
|
272 |
+
dto.success = True
|
273 |
+
dto.original_text = "Test"
|
274 |
+
dto.translated_text = None
|
275 |
+
dto.audio_path = None
|
276 |
+
dto.processing_time = 0.0
|
277 |
+
dto.error_message = None
|
278 |
+
dto.error_code = None
|
279 |
+
dto.metadata = {}
|
280 |
+
dto.timestamp = None
|
281 |
+
|
282 |
+
result = dto.to_dict()
|
283 |
+
|
284 |
+
assert result['timestamp'] is None
|
285 |
+
|
286 |
+
def test_success_result_class_method(self):
|
287 |
+
"""Test success_result class method"""
|
288 |
+
result = ProcessingResultDto.success_result(
|
289 |
+
original_text="Hello world",
|
290 |
+
translated_text="Hola mundo",
|
291 |
+
audio_path="/tmp/output.wav",
|
292 |
+
processing_time=3.5,
|
293 |
+
metadata={"test": "value"}
|
294 |
+
)
|
295 |
+
|
296 |
+
assert isinstance(result, ProcessingResultDto)
|
297 |
+
assert result.success is True
|
298 |
+
assert result.original_text == "Hello world"
|
299 |
+
assert result.translated_text == "Hola mundo"
|
300 |
+
assert result.audio_path == "/tmp/output.wav"
|
301 |
+
assert result.processing_time == 3.5
|
302 |
+
assert result.metadata == {"test": "value"}
|
303 |
+
assert result.error_message is None
|
304 |
+
assert result.error_code is None
|
305 |
+
|
306 |
+
def test_success_result_class_method_with_defaults(self):
|
307 |
+
"""Test success_result class method with default values"""
|
308 |
+
result = ProcessingResultDto.success_result(original_text="Test")
|
309 |
+
|
310 |
+
assert result.success is True
|
311 |
+
assert result.original_text == "Test"
|
312 |
+
assert result.translated_text is None
|
313 |
+
assert result.audio_path is None
|
314 |
+
assert result.processing_time == 0.0
|
315 |
+
assert result.metadata is None
|
316 |
+
|
317 |
+
def test_error_result_class_method(self):
|
318 |
+
"""Test error_result class method"""
|
319 |
+
result = ProcessingResultDto.error_result(
|
320 |
+
error_message="Processing failed",
|
321 |
+
error_code="STT_ERROR",
|
322 |
+
processing_time=2.1,
|
323 |
+
metadata={"correlation_id": "test-456"}
|
324 |
+
)
|
325 |
+
|
326 |
+
assert isinstance(result, ProcessingResultDto)
|
327 |
+
assert result.success is False
|
328 |
+
assert result.error_message == "Processing failed"
|
329 |
+
assert result.error_code == "STT_ERROR"
|
330 |
+
assert result.processing_time == 2.1
|
331 |
+
assert result.metadata == {"correlation_id": "test-456"}
|
332 |
+
assert result.original_text is None
|
333 |
+
assert result.translated_text is None
|
334 |
+
assert result.audio_path is None
|
335 |
+
|
336 |
+
def test_error_result_class_method_with_defaults(self):
|
337 |
+
"""Test error_result class method with default values"""
|
338 |
+
result = ProcessingResultDto.error_result("Test error")
|
339 |
+
|
340 |
+
assert result.success is False
|
341 |
+
assert result.error_message == "Test error"
|
342 |
+
assert result.error_code is None
|
343 |
+
assert result.processing_time == 0.0
|
344 |
+
assert result.metadata is None
|
345 |
+
|
346 |
+
def test_from_dict_class_method(self):
|
347 |
+
"""Test from_dict class method"""
|
348 |
+
test_timestamp = datetime(2023, 1, 1, 12, 0, 0)
|
349 |
+
|
350 |
+
data = {
|
351 |
+
'success': True,
|
352 |
+
'original_text': 'Hello world',
|
353 |
+
'translated_text': 'Hola mundo',
|
354 |
+
'audio_path': '/tmp/output.wav',
|
355 |
+
'processing_time': 5.2,
|
356 |
+
'error_message': None,
|
357 |
+
'error_code': None,
|
358 |
+
'metadata': {'correlation_id': 'test-123'},
|
359 |
+
'timestamp': test_timestamp.isoformat()
|
360 |
+
}
|
361 |
+
|
362 |
+
dto = ProcessingResultDto.from_dict(data)
|
363 |
+
|
364 |
+
assert dto.success is True
|
365 |
+
assert dto.original_text == 'Hello world'
|
366 |
+
assert dto.translated_text == 'Hola mundo'
|
367 |
+
assert dto.audio_path == '/tmp/output.wav'
|
368 |
+
assert dto.processing_time == 5.2
|
369 |
+
assert dto.error_message is None
|
370 |
+
assert dto.error_code is None
|
371 |
+
assert dto.metadata == {'correlation_id': 'test-123'}
|
372 |
+
assert dto.timestamp == test_timestamp
|
373 |
+
|
374 |
+
def test_from_dict_class_method_with_z_timestamp(self):
|
375 |
+
"""Test from_dict class method with Z-suffixed timestamp"""
|
376 |
+
data = {
|
377 |
+
'success': True,
|
378 |
+
'original_text': 'Test',
|
379 |
+
'timestamp': '2023-01-01T12:00:00Z'
|
380 |
+
}
|
381 |
+
|
382 |
+
dto = ProcessingResultDto.from_dict(data)
|
383 |
+
|
384 |
+
assert dto.timestamp == datetime(2023, 1, 1, 12, 0, 0)
|
385 |
+
|
386 |
+
def test_from_dict_class_method_with_defaults(self):
|
387 |
+
"""Test from_dict class method with default values"""
|
388 |
+
data = {
|
389 |
+
'success': True,
|
390 |
+
'original_text': 'Test'
|
391 |
+
}
|
392 |
+
|
393 |
+
dto = ProcessingResultDto.from_dict(data)
|
394 |
+
|
395 |
+
assert dto.success is True
|
396 |
+
assert dto.original_text == 'Test'
|
397 |
+
assert dto.translated_text is None
|
398 |
+
assert dto.audio_path is None
|
399 |
+
assert dto.processing_time == 0.0
|
400 |
+
assert dto.error_message is None
|
401 |
+
assert dto.error_code is None
|
402 |
+
assert dto.metadata is None
|
403 |
+
assert dto.timestamp is None
|
404 |
+
|
405 |
+
def test_validation_called_on_init(self):
|
406 |
+
"""Test that validation is called during initialization"""
|
407 |
+
# This should trigger validation and raise an error
|
408 |
+
with pytest.raises(ValueError):
|
409 |
+
ProcessingResultDto(
|
410 |
+
success="invalid", # Invalid type
|
411 |
+
original_text="Test"
|
412 |
+
)
|
413 |
+
|
414 |
+
def test_metadata_default_initialization(self):
|
415 |
+
"""Test that metadata is initialized to empty dict if None"""
|
416 |
+
dto = ProcessingResultDto(
|
417 |
+
success=True,
|
418 |
+
original_text="Test",
|
419 |
+
metadata=None
|
420 |
+
)
|
421 |
+
|
422 |
+
assert dto.metadata == {}
|
423 |
+
|
424 |
+
def test_timestamp_default_initialization(self):
|
425 |
+
"""Test that timestamp is initialized to current time if None"""
|
426 |
+
before = datetime.utcnow()
|
427 |
+
|
428 |
+
dto = ProcessingResultDto(
|
429 |
+
success=True,
|
430 |
+
original_text="Test",
|
431 |
+
timestamp=None
|
432 |
+
)
|
433 |
+
|
434 |
+
after = datetime.utcnow()
|
435 |
+
|
436 |
+
assert before <= dto.timestamp <= after
|
tests/unit/application/services/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
"""Unit tests for application services"""
|
tests/unit/application/services/test_audio_processing_service.py
ADDED
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Unit tests for AudioProcessingApplicationService"""
|
2 |
+
|
3 |
+
import pytest
|
4 |
+
import tempfile
|
5 |
+
import os
|
6 |
+
import time
|
7 |
+
from unittest.mock import Mock, MagicMock, patch, call
|
8 |
+
from contextlib import contextmanager
|
9 |
+
|
10 |
+
from src.application.services.audio_processing_service import AudioProcessingApplicationService
|
11 |
+
from src.application.dtos.audio_upload_dto import AudioUploadDto
|
12 |
+
from src.application.dtos.processing_request_dto import ProcessingRequestDto
|
13 |
+
from src.application.dtos.processing_result_dto import ProcessingResultDto
|
14 |
+
from src.domain.models.audio_content import AudioContent
|
15 |
+
from src.domain.models.text_content import TextContent
|
16 |
+
from src.domain.models.translation_request import TranslationRequest
|
17 |
+
from src.domain.models.speech_synthesis_request import SpeechSynthesisRequest
|
18 |
+
from src.domain.models.voice_settings import VoiceSettings
|
19 |
+
from src.domain.exceptions import (
|
20 |
+
AudioProcessingException,
|
21 |
+
SpeechRecognitionException,
|
22 |
+
TranslationFailedException,
|
23 |
+
SpeechSynthesisException
|
24 |
+
)
|
25 |
+
from src.infrastructure.config.app_config import AppConfig
|
26 |
+
from src.infrastructure.config.dependency_container import DependencyContainer
|
27 |
+
|
28 |
+
|
29 |
+
class TestAudioProcessingApplicationService:
|
30 |
+
"""Test cases for AudioProcessingApplicationService"""
|
31 |
+
|
32 |
+
@pytest.fixture
|
33 |
+
def mock_container(self):
|
34 |
+
"""Create mock dependency container"""
|
35 |
+
container = Mock(spec=DependencyContainer)
|
36 |
+
|
37 |
+
# Mock providers
|
38 |
+
mock_stt_provider = Mock()
|
39 |
+
mock_translation_provider = Mock()
|
40 |
+
mock_tts_provider = Mock()
|
41 |
+
|
42 |
+
container.get_stt_provider.return_value = mock_stt_provider
|
43 |
+
container.get_translation_provider.return_value = mock_translation_provider
|
44 |
+
container.get_tts_provider.return_value = mock_tts_provider
|
45 |
+
|
46 |
+
return container
|
47 |
+
|
48 |
+
@pytest.fixture
|
49 |
+
def mock_config(self):
|
50 |
+
"""Create mock application config"""
|
51 |
+
config = Mock(spec=AppConfig)
|
52 |
+
|
53 |
+
# Mock configuration methods
|
54 |
+
config.get_logging_config.return_value = {
|
55 |
+
'level': 'INFO',
|
56 |
+
'enable_file_logging': False,
|
57 |
+
'log_file_path': '/tmp/test.log',
|
58 |
+
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
59 |
+
}
|
60 |
+
|
61 |
+
config.get_processing_config.return_value = {
|
62 |
+
'max_file_size_mb': 100,
|
63 |
+
'supported_audio_formats': ['wav', 'mp3', 'flac', 'ogg', 'm4a'],
|
64 |
+
'temp_dir': '/tmp',
|
65 |
+
'cleanup_temp_files': True,
|
66 |
+
'processing_timeout_seconds': 300
|
67 |
+
}
|
68 |
+
|
69 |
+
config.get_stt_config.return_value = {
|
70 |
+
'preferred_providers': ['whisper-small', 'whisper-medium']
|
71 |
+
}
|
72 |
+
|
73 |
+
config.get_tts_config.return_value = {
|
74 |
+
'preferred_providers': ['kokoro', 'dia']
|
75 |
+
}
|
76 |
+
|
77 |
+
return config
|
78 |
+
|
79 |
+
@pytest.fixture
|
80 |
+
def sample_audio_upload(self):
|
81 |
+
"""Create sample audio upload DTO"""
|
82 |
+
return AudioUploadDto(
|
83 |
+
filename="test_audio.wav",
|
84 |
+
content=b"fake_audio_content_" + b"x" * 1000, # 1KB+ of fake audio
|
85 |
+
content_type="audio/wav"
|
86 |
+
)
|
87 |
+
|
88 |
+
@pytest.fixture
|
89 |
+
def sample_processing_request(self, sample_audio_upload):
|
90 |
+
"""Create sample processing request DTO"""
|
91 |
+
return ProcessingRequestDto(
|
92 |
+
audio=sample_audio_upload,
|
93 |
+
asr_model="whisper-small",
|
94 |
+
target_language="es",
|
95 |
+
voice="kokoro",
|
96 |
+
speed=1.0,
|
97 |
+
source_language="en"
|
98 |
+
)
|
99 |
+
|
100 |
+
@pytest.fixture
|
101 |
+
def service(self, mock_container, mock_config):
|
102 |
+
"""Create AudioProcessingApplicationService instance"""
|
103 |
+
mock_container.resolve.return_value = mock_config
|
104 |
+
return AudioProcessingApplicationService(mock_container, mock_config)
|
105 |
+
|
106 |
+
def test_initialization(self, mock_container, mock_config):
|
107 |
+
"""Test service initialization"""
|
108 |
+
service = AudioProcessingApplicationService(mock_container, mock_config)
|
109 |
+
|
110 |
+
assert service._container == mock_container
|
111 |
+
assert service._config == mock_config
|
112 |
+
assert service._temp_files == {}
|
113 |
+
assert service._error_mapper is not None
|
114 |
+
assert service._recovery_manager is not None
|
115 |
+
|
116 |
+
def test_initialization_without_config(self, mock_container, mock_config):
|
117 |
+
"""Test service initialization without explicit config"""
|
118 |
+
mock_container.resolve.return_value = mock_config
|
119 |
+
|
120 |
+
service = AudioProcessingApplicationService(mock_container)
|
121 |
+
|
122 |
+
assert service._container == mock_container
|
123 |
+
assert service._config == mock_config
|
124 |
+
mock_container.resolve.assert_called_once_with(AppConfig)
|
125 |
+
|
126 |
+
@patch('src.application.services.audio_processing_service.get_structured_logger')
|
127 |
+
def test_setup_logging_success(self, mock_logger, service, mock_config):
|
128 |
+
"""Test successful logging setup"""
|
129 |
+
mock_config.get_logging_config.return_value = {
|
130 |
+
'level': 'DEBUG',
|
131 |
+
'enable_file_logging': True,
|
132 |
+
'log_file_path': '/tmp/test.log',
|
133 |
+
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
134 |
+
}
|
135 |
+
|
136 |
+
service._setup_logging()
|
137 |
+
|
138 |
+
# Verify logging configuration was retrieved
|
139 |
+
mock_config.get_logging_config.assert_called_once()
|
140 |
+
|
141 |
+
@patch('src.application.services.audio_processing_service.get_structured_logger')
|
142 |
+
def test_setup_logging_failure(self, mock_logger, service, mock_config):
|
143 |
+
"""Test logging setup failure handling"""
|
144 |
+
mock_config.get_logging_config.side_effect = Exception("Config error")
|
145 |
+
|
146 |
+
# Should not raise exception
|
147 |
+
service._setup_logging()
|
148 |
+
|
149 |
+
# Warning should be logged
|
150 |
+
mock_logger.return_value.warning.assert_called_once()
|
151 |
+
|
152 |
+
def test_validate_request_success(self, service, sample_processing_request):
|
153 |
+
"""Test successful request validation"""
|
154 |
+
# Should not raise exception
|
155 |
+
service._validate_request(sample_processing_request)
|
156 |
+
|
157 |
+
def test_validate_request_invalid_type(self, service):
|
158 |
+
"""Test request validation with invalid type"""
|
159 |
+
with pytest.raises(ValueError, match="Request must be a ProcessingRequestDto instance"):
|
160 |
+
service._validate_request("invalid_request")
|
161 |
+
|
162 |
+
def test_validate_request_file_too_large(self, service, sample_processing_request, mock_config):
|
163 |
+
"""Test request validation with file too large"""
|
164 |
+
mock_config.get_processing_config.return_value['max_file_size_mb'] = 0.001 # Very small limit
|
165 |
+
|
166 |
+
with pytest.raises(ValueError, match="Audio file too large"):
|
167 |
+
service._validate_request(sample_processing_request)
|
168 |
+
|
169 |
+
def test_validate_request_unsupported_format(self, service, sample_processing_request, mock_config):
|
170 |
+
"""Test request validation with unsupported format"""
|
171 |
+
sample_processing_request.audio.filename = "test.xyz"
|
172 |
+
mock_config.get_processing_config.return_value['supported_audio_formats'] = ['wav', 'mp3']
|
173 |
+
|
174 |
+
with pytest.raises(ValueError, match="Unsupported audio format"):
|
175 |
+
service._validate_request(sample_processing_request)
|
176 |
+
|
177 |
+
@patch('os.makedirs')
|
178 |
+
@patch('shutil.rmtree')
|
179 |
+
def test_create_temp_directory(self, mock_rmtree, mock_makedirs, service):
|
180 |
+
"""Test temporary directory creation and cleanup"""
|
181 |
+
correlation_id = "test-123"
|
182 |
+
|
183 |
+
with service._create_temp_directory(correlation_id) as temp_dir:
|
184 |
+
assert correlation_id in temp_dir
|
185 |
+
mock_makedirs.assert_called_once()
|
186 |
+
|
187 |
+
# Cleanup should be called
|
188 |
+
mock_rmtree.assert_called_once()
|
189 |
+
|
190 |
+
@patch('builtins.open', create=True)
|
191 |
+
def test_convert_upload_to_audio_content_success(self, mock_open, service, sample_audio_upload):
|
192 |
+
"""Test successful audio upload conversion"""
|
193 |
+
temp_dir = "/tmp/test"
|
194 |
+
mock_file = MagicMock()
|
195 |
+
mock_open.return_value.__enter__.return_value = mock_file
|
196 |
+
|
197 |
+
result = service._convert_upload_to_audio_content(sample_audio_upload, temp_dir)
|
198 |
+
|
199 |
+
assert isinstance(result, AudioContent)
|
200 |
+
assert result.data == sample_audio_upload.content
|
201 |
+
assert result.format == "wav"
|
202 |
+
mock_file.write.assert_called_once_with(sample_audio_upload.content)
|
203 |
+
|
204 |
+
@patch('builtins.open', side_effect=IOError("File error"))
|
205 |
+
def test_convert_upload_to_audio_content_failure(self, mock_open, service, sample_audio_upload):
|
206 |
+
"""Test audio upload conversion failure"""
|
207 |
+
temp_dir = "/tmp/test"
|
208 |
+
|
209 |
+
with pytest.raises(AudioProcessingException, match="Failed to process uploaded audio"):
|
210 |
+
service._convert_upload_to_audio_content(sample_audio_upload, temp_dir)
|
211 |
+
|
212 |
+
def test_perform_speech_recognition_success(self, service, mock_container):
|
213 |
+
"""Test successful speech recognition"""
|
214 |
+
audio = AudioContent(data=b"audio", format="wav", sample_rate=16000, duration=1.0)
|
215 |
+
model = "whisper-small"
|
216 |
+
correlation_id = "test-123"
|
217 |
+
|
218 |
+
# Mock STT provider
|
219 |
+
mock_stt_provider = Mock()
|
220 |
+
expected_text = TextContent(text="Hello world", language="en")
|
221 |
+
mock_stt_provider.transcribe.return_value = expected_text
|
222 |
+
mock_container.get_stt_provider.return_value = mock_stt_provider
|
223 |
+
|
224 |
+
result = service._perform_speech_recognition(audio, model, correlation_id)
|
225 |
+
|
226 |
+
assert result == expected_text
|
227 |
+
mock_container.get_stt_provider.assert_called_once_with(model)
|
228 |
+
mock_stt_provider.transcribe.assert_called_once_with(audio, model)
|
229 |
+
|
230 |
+
def test_perform_speech_recognition_failure(self, service, mock_container):
|
231 |
+
"""Test speech recognition failure"""
|
232 |
+
audio = AudioContent(data=b"audio", format="wav", sample_rate=16000, duration=1.0)
|
233 |
+
model = "whisper-small"
|
234 |
+
correlation_id = "test-123"
|
235 |
+
|
236 |
+
# Mock STT provider to raise exception
|
237 |
+
mock_stt_provider = Mock()
|
238 |
+
mock_stt_provider.transcribe.side_effect = Exception("STT failed")
|
239 |
+
mock_container.get_stt_provider.return_value = mock_stt_provider
|
240 |
+
|
241 |
+
with pytest.raises(SpeechRecognitionException, match="Speech recognition failed"):
|
242 |
+
service._perform_speech_recognition(audio, model, correlation_id)
|
243 |
+
|
244 |
+
def test_perform_translation_success(self, service, mock_container):
|
245 |
+
"""Test successful translation"""
|
246 |
+
text = TextContent(text="Hello world", language="en")
|
247 |
+
source_language = "en"
|
248 |
+
target_language = "es"
|
249 |
+
correlation_id = "test-123"
|
250 |
+
|
251 |
+
# Mock translation provider
|
252 |
+
mock_translation_provider = Mock()
|
253 |
+
expected_text = TextContent(text="Hola mundo", language="es")
|
254 |
+
mock_translation_provider.translate.return_value = expected_text
|
255 |
+
mock_container.get_translation_provider.return_value = mock_translation_provider
|
256 |
+
|
257 |
+
result = service._perform_translation(text, source_language, target_language, correlation_id)
|
258 |
+
|
259 |
+
assert result == expected_text
|
260 |
+
mock_container.get_translation_provider.assert_called_once()
|
261 |
+
mock_translation_provider.translate.assert_called_once()
|
262 |
+
|
263 |
+
def test_perform_translation_failure(self, service, mock_container):
|
264 |
+
"""Test translation failure"""
|
265 |
+
text = TextContent(text="Hello world", language="en")
|
266 |
+
source_language = "en"
|
267 |
+
target_language = "es"
|
268 |
+
correlation_id = "test-123"
|
269 |
+
|
270 |
+
# Mock translation provider to raise exception
|
271 |
+
mock_translation_provider = Mock()
|
272 |
+
mock_translation_provider.translate.side_effect = Exception("Translation failed")
|
273 |
+
mock_container.get_translation_provider.return_value = mock_translation_provider
|
274 |
+
|
275 |
+
with pytest.raises(TranslationFailedException, match="Translation failed"):
|
276 |
+
service._perform_translation(text, source_language, target_language, correlation_id)
|
277 |
+
|
278 |
+
@patch('builtins.open', create=True)
|
279 |
+
def test_perform_speech_synthesis_success(self, mock_open, service, mock_container):
|
280 |
+
"""Test successful speech synthesis"""
|
281 |
+
text = TextContent(text="Hola mundo", language="es")
|
282 |
+
voice = "kokoro"
|
283 |
+
speed = 1.0
|
284 |
+
language = "es"
|
285 |
+
temp_dir = "/tmp/test"
|
286 |
+
correlation_id = "test-123"
|
287 |
+
|
288 |
+
# Mock TTS provider
|
289 |
+
mock_tts_provider = Mock()
|
290 |
+
mock_audio = AudioContent(data=b"synthesized_audio", format="wav", sample_rate=22050, duration=2.0)
|
291 |
+
mock_tts_provider.synthesize.return_value = mock_audio
|
292 |
+
mock_container.get_tts_provider.return_value = mock_tts_provider
|
293 |
+
|
294 |
+
# Mock file operations
|
295 |
+
mock_file = MagicMock()
|
296 |
+
mock_open.return_value.__enter__.return_value = mock_file
|
297 |
+
|
298 |
+
result = service._perform_speech_synthesis(text, voice, speed, language, temp_dir, correlation_id)
|
299 |
+
|
300 |
+
assert correlation_id in result
|
301 |
+
assert result.endswith(".wav")
|
302 |
+
mock_container.get_tts_provider.assert_called_once_with(voice)
|
303 |
+
mock_tts_provider.synthesize.assert_called_once()
|
304 |
+
mock_file.write.assert_called_once_with(mock_audio.data)
|
305 |
+
|
306 |
+
def test_perform_speech_synthesis_failure(self, service, mock_container):
|
307 |
+
"""Test speech synthesis failure"""
|
308 |
+
text = TextContent(text="Hola mundo", language="es")
|
309 |
+
voice = "kokoro"
|
310 |
+
speed = 1.0
|
311 |
+
language = "es"
|
312 |
+
temp_dir = "/tmp/test"
|
313 |
+
correlation_id = "test-123"
|
314 |
+
|
315 |
+
# Mock TTS provider to raise exception
|
316 |
+
mock_tts_provider = Mock()
|
317 |
+
mock_tts_provider.synthesize.side_effect = Exception("TTS failed")
|
318 |
+
mock_container.get_tts_provider.return_value = mock_tts_provider
|
319 |
+
|
320 |
+
with pytest.raises(SpeechSynthesisException, match="Speech synthesis failed"):
|
321 |
+
service._perform_speech_synthesis(text, voice, speed, language, temp_dir, correlation_id)
|
322 |
+
|
323 |
+
def test_get_error_code_from_exception(self, service):
|
324 |
+
"""Test error code mapping from exceptions"""
|
325 |
+
assert service._get_error_code_from_exception(SpeechRecognitionException("test")) == 'STT_ERROR'
|
326 |
+
assert service._get_error_code_from_exception(TranslationFailedException("test")) == 'TRANSLATION_ERROR'
|
327 |
+
assert service._get_error_code_from_exception(SpeechSynthesisException("test")) == 'TTS_ERROR'
|
328 |
+
assert service._get_error_code_from_exception(ValueError("test")) == 'VALIDATION_ERROR'
|
329 |
+
assert service._get_error_code_from_exception(Exception("test")) == 'SYSTEM_ERROR'
|
330 |
+
|
331 |
+
@patch('os.path.exists')
|
332 |
+
@patch('os.remove')
|
333 |
+
def test_cleanup_temp_files_success(self, mock_remove, mock_exists, service):
|
334 |
+
"""Test successful temporary file cleanup"""
|
335 |
+
service._temp_files = {
|
336 |
+
"/tmp/file1.wav": "/tmp/file1.wav",
|
337 |
+
"/tmp/file2.wav": "/tmp/file2.wav"
|
338 |
+
}
|
339 |
+
mock_exists.return_value = True
|
340 |
+
|
341 |
+
service._cleanup_temp_files()
|
342 |
+
|
343 |
+
assert service._temp_files == {}
|
344 |
+
assert mock_remove.call_count == 2
|
345 |
+
|
346 |
+
@patch('os.path.exists')
|
347 |
+
@patch('os.remove', side_effect=OSError("Permission denied"))
|
348 |
+
def test_cleanup_temp_files_failure(self, mock_remove, mock_exists, service):
|
349 |
+
"""Test temporary file cleanup with failures"""
|
350 |
+
service._temp_files = {"/tmp/file1.wav": "/tmp/file1.wav"}
|
351 |
+
mock_exists.return_value = True
|
352 |
+
|
353 |
+
# Should not raise exception
|
354 |
+
service._cleanup_temp_files()
|
355 |
+
|
356 |
+
# File should still be removed from tracking
|
357 |
+
assert service._temp_files == {}
|
358 |
+
|
359 |
+
def test_get_processing_status(self, service):
|
360 |
+
"""Test processing status retrieval"""
|
361 |
+
correlation_id = "test-123"
|
362 |
+
|
363 |
+
result = service.get_processing_status(correlation_id)
|
364 |
+
|
365 |
+
assert result['correlation_id'] == correlation_id
|
366 |
+
assert 'status' in result
|
367 |
+
assert 'message' in result
|
368 |
+
|
369 |
+
def test_get_supported_configurations(self, service):
|
370 |
+
"""Test supported configurations retrieval"""
|
371 |
+
result = service.get_supported_configurations()
|
372 |
+
|
373 |
+
assert 'asr_models' in result
|
374 |
+
assert 'voices' in result
|
375 |
+
assert 'languages' in result
|
376 |
+
assert 'audio_formats' in result
|
377 |
+
assert 'max_file_size_mb' in result
|
378 |
+
assert 'speed_range' in result
|
379 |
+
|
380 |
+
# Verify expected values
|
381 |
+
assert 'whisper-small' in result['asr_models']
|
382 |
+
assert 'kokoro' in result['voices']
|
383 |
+
assert 'en' in result['languages']
|
384 |
+
|
385 |
+
def test_cleanup(self, service):
|
386 |
+
"""Test service cleanup"""
|
387 |
+
service._temp_files = {"/tmp/test.wav": "/tmp/test.wav"}
|
388 |
+
|
389 |
+
with patch.object(service, '_cleanup_temp_files') as mock_cleanup:
|
390 |
+
service.cleanup()
|
391 |
+
mock_cleanup.assert_called_once()
|
392 |
+
|
393 |
+
def test_context_manager(self, service):
|
394 |
+
"""Test service as context manager"""
|
395 |
+
with patch.object(service, 'cleanup') as mock_cleanup:
|
396 |
+
with service as svc:
|
397 |
+
assert svc == service
|
398 |
+
mock_cleanup.assert_called_once()
|
399 |
+
|
400 |
+
@patch('src.application.services.audio_processing_service.time.time')
|
401 |
+
@patch.object(AudioProcessingApplicationService, '_create_temp_directory')
|
402 |
+
@patch.object(AudioProcessingApplicationService, '_convert_upload_to_audio_content')
|
403 |
+
@patch.object(AudioProcessingApplicationService, '_perform_speech_recognition_with_recovery')
|
404 |
+
@patch.object(AudioProcessingApplicationService, '_perform_translation_with_recovery')
|
405 |
+
@patch.object(AudioProcessingApplicationService, '_perform_speech_synthesis_with_recovery')
|
406 |
+
def test_process_audio_pipeline_success(self, mock_tts, mock_translation, mock_stt,
|
407 |
+
mock_convert, mock_temp_dir, mock_time,
|
408 |
+
service, sample_processing_request):
|
409 |
+
"""Test successful audio processing pipeline"""
|
410 |
+
# Setup mocks
|
411 |
+
mock_time.side_effect = [0.0, 5.0] # Start and end times
|
412 |
+
mock_temp_dir.return_value.__enter__.return_value = "/tmp/test"
|
413 |
+
mock_temp_dir.return_value.__exit__.return_value = None
|
414 |
+
|
415 |
+
mock_audio = AudioContent(data=b"audio", format="wav", sample_rate=16000, duration=1.0)
|
416 |
+
mock_convert.return_value = mock_audio
|
417 |
+
|
418 |
+
mock_original_text = TextContent(text="Hello world", language="en")
|
419 |
+
mock_stt.return_value = mock_original_text
|
420 |
+
|
421 |
+
mock_translated_text = TextContent(text="Hola mundo", language="es")
|
422 |
+
mock_translation.return_value = mock_translated_text
|
423 |
+
|
424 |
+
mock_tts.return_value = "/tmp/test/output_123.wav"
|
425 |
+
|
426 |
+
with patch.object(service, '_validate_request'):
|
427 |
+
result = service.process_audio_pipeline(sample_processing_request)
|
428 |
+
|
429 |
+
# Verify result
|
430 |
+
assert isinstance(result, ProcessingResultDto)
|
431 |
+
assert result.success is True
|
432 |
+
assert result.original_text == "Hello world"
|
433 |
+
assert result.translated_text == "Hola mundo"
|
434 |
+
assert result.audio_path == "/tmp/test/output_123.wav"
|
435 |
+
assert result.processing_time == 5.0
|
436 |
+
|
437 |
+
@patch('src.application.services.audio_processing_service.time.time')
|
438 |
+
def test_process_audio_pipeline_validation_error(self, mock_time, service, sample_processing_request):
|
439 |
+
"""Test audio processing pipeline with validation error"""
|
440 |
+
mock_time.side_effect = [0.0, 1.0]
|
441 |
+
|
442 |
+
with patch.object(service, '_validate_request', side_effect=ValueError("Invalid request")):
|
443 |
+
result = service.process_audio_pipeline(sample_processing_request)
|
444 |
+
|
445 |
+
# Verify error result
|
446 |
+
assert isinstance(result, ProcessingResultDto)
|
447 |
+
assert result.success is False
|
448 |
+
assert "Invalid request" in result.error_message
|
449 |
+
assert result.processing_time == 1.0
|
450 |
+
|
451 |
+
@patch('src.application.services.audio_processing_service.time.time')
|
452 |
+
def test_process_audio_pipeline_domain_exception(self, mock_time, service, sample_processing_request):
|
453 |
+
"""Test audio processing pipeline with domain exception"""
|
454 |
+
mock_time.side_effect = [0.0, 2.0]
|
455 |
+
|
456 |
+
with patch.object(service, '_validate_request'):
|
457 |
+
with patch.object(service, '_create_temp_directory', side_effect=SpeechRecognitionException("STT failed")):
|
458 |
+
result = service.process_audio_pipeline(sample_processing_request)
|
459 |
+
|
460 |
+
# Verify error result
|
461 |
+
assert isinstance(result, ProcessingResultDto)
|
462 |
+
assert result.success is False
|
463 |
+
assert result.error_message is not None
|
464 |
+
assert result.processing_time == 2.0
|
465 |
+
|
466 |
+
def test_recovery_methods_exist(self, service):
|
467 |
+
"""Test that recovery methods exist and are callable"""
|
468 |
+
# These methods should exist for error recovery
|
469 |
+
assert hasattr(service, '_perform_speech_recognition_with_recovery')
|
470 |
+
assert hasattr(service, '_perform_translation_with_recovery')
|
471 |
+
assert hasattr(service, '_perform_speech_synthesis_with_recovery')
|
472 |
+
|
473 |
+
assert callable(service._perform_speech_recognition_with_recovery)
|
474 |
+
assert callable(service._perform_translation_with_recovery)
|
475 |
+
assert callable(service._perform_speech_synthesis_with_recovery)
|
tests/unit/application/services/test_configuration_service.py
ADDED
@@ -0,0 +1,572 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Unit tests for ConfigurationApplicationService"""
|
2 |
+
|
3 |
+
import pytest
|
4 |
+
import os
|
5 |
+
import json
|
6 |
+
from unittest.mock import Mock, MagicMock, patch, mock_open
|
7 |
+
|
8 |
+
from src.application.services.configuration_service import (
|
9 |
+
ConfigurationApplicationService,
|
10 |
+
ConfigurationException
|
11 |
+
)
|
12 |
+
from src.infrastructure.config.app_config import AppConfig
|
13 |
+
from src.infrastructure.config.dependency_container import DependencyContainer
|
14 |
+
|
15 |
+
|
16 |
+
class TestConfigurationApplicationService:
|
17 |
+
"""Test cases for ConfigurationApplicationService"""
|
18 |
+
|
19 |
+
@pytest.fixture
|
20 |
+
def mock_container(self):
|
21 |
+
"""Create mock dependency container"""
|
22 |
+
container = Mock(spec=DependencyContainer)
|
23 |
+
return container
|
24 |
+
|
25 |
+
@pytest.fixture
|
26 |
+
def mock_config(self):
|
27 |
+
"""Create mock application config"""
|
28 |
+
config = Mock(spec=AppConfig)
|
29 |
+
|
30 |
+
# Mock configuration methods
|
31 |
+
config.get_tts_config.return_value = {
|
32 |
+
'preferred_providers': ['kokoro', 'dia'],
|
33 |
+
'default_speed': 1.0,
|
34 |
+
'default_language': 'en',
|
35 |
+
'enable_streaming': False,
|
36 |
+
'max_text_length': 5000
|
37 |
+
}
|
38 |
+
|
39 |
+
config.get_stt_config.return_value = {
|
40 |
+
'preferred_providers': ['whisper', 'parakeet'],
|
41 |
+
'default_model': 'whisper',
|
42 |
+
'chunk_length_s': 30,
|
43 |
+
'batch_size': 16,
|
44 |
+
'enable_vad': True
|
45 |
+
}
|
46 |
+
|
47 |
+
config.get_translation_config.return_value = {
|
48 |
+
'default_provider': 'nllb',
|
49 |
+
'model_name': 'nllb-200-3.3B',
|
50 |
+
'max_chunk_length': 512,
|
51 |
+
'batch_size': 8,
|
52 |
+
'cache_translations': True
|
53 |
+
}
|
54 |
+
|
55 |
+
config.get_processing_config.return_value = {
|
56 |
+
'temp_dir': '/tmp',
|
57 |
+
'cleanup_temp_files': True,
|
58 |
+
'max_file_size_mb': 100,
|
59 |
+
'supported_audio_formats': ['wav', 'mp3', 'flac'],
|
60 |
+
'processing_timeout_seconds': 300
|
61 |
+
}
|
62 |
+
|
63 |
+
config.get_logging_config.return_value = {
|
64 |
+
'level': 'INFO',
|
65 |
+
'enable_file_logging': False,
|
66 |
+
'log_file_path': '/tmp/app.log',
|
67 |
+
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
68 |
+
}
|
69 |
+
|
70 |
+
# Mock config objects for attribute access
|
71 |
+
config.tts = Mock()
|
72 |
+
config.stt = Mock()
|
73 |
+
config.translation = Mock()
|
74 |
+
config.processing = Mock()
|
75 |
+
config.logging = Mock()
|
76 |
+
|
77 |
+
return config
|
78 |
+
|
79 |
+
@pytest.fixture
|
80 |
+
def service(self, mock_container, mock_config):
|
81 |
+
"""Create ConfigurationApplicationService instance"""
|
82 |
+
mock_container.resolve.return_value = mock_config
|
83 |
+
return ConfigurationApplicationService(mock_container, mock_config)
|
84 |
+
|
85 |
+
def test_initialization(self, mock_container, mock_config):
|
86 |
+
"""Test service initialization"""
|
87 |
+
service = ConfigurationApplicationService(mock_container, mock_config)
|
88 |
+
|
89 |
+
assert service._container == mock_container
|
90 |
+
assert service._config == mock_config
|
91 |
+
assert service._error_mapper is not None
|
92 |
+
|
93 |
+
def test_initialization_without_config(self, mock_container, mock_config):
|
94 |
+
"""Test service initialization without explicit config"""
|
95 |
+
mock_container.resolve.return_value = mock_config
|
96 |
+
|
97 |
+
service = ConfigurationApplicationService(mock_container)
|
98 |
+
|
99 |
+
assert service._container == mock_container
|
100 |
+
assert service._config == mock_config
|
101 |
+
mock_container.resolve.assert_called_once_with(AppConfig)
|
102 |
+
|
103 |
+
def test_get_current_configuration_success(self, service, mock_config):
|
104 |
+
"""Test successful current configuration retrieval"""
|
105 |
+
result = service.get_current_configuration()
|
106 |
+
|
107 |
+
assert 'tts' in result
|
108 |
+
assert 'stt' in result
|
109 |
+
assert 'translation' in result
|
110 |
+
assert 'processing' in result
|
111 |
+
assert 'logging' in result
|
112 |
+
|
113 |
+
# Verify all config methods were called
|
114 |
+
mock_config.get_tts_config.assert_called_once()
|
115 |
+
mock_config.get_stt_config.assert_called_once()
|
116 |
+
mock_config.get_translation_config.assert_called_once()
|
117 |
+
mock_config.get_processing_config.assert_called_once()
|
118 |
+
mock_config.get_logging_config.assert_called_once()
|
119 |
+
|
120 |
+
def test_get_current_configuration_failure(self, service, mock_config):
|
121 |
+
"""Test current configuration retrieval failure"""
|
122 |
+
mock_config.get_tts_config.side_effect = Exception("Config error")
|
123 |
+
|
124 |
+
with pytest.raises(ConfigurationException, match="Failed to retrieve configuration"):
|
125 |
+
service.get_current_configuration()
|
126 |
+
|
127 |
+
def test_get_tts_configuration_success(self, service, mock_config):
|
128 |
+
"""Test successful TTS configuration retrieval"""
|
129 |
+
result = service.get_tts_configuration()
|
130 |
+
|
131 |
+
assert result['preferred_providers'] == ['kokoro', 'dia']
|
132 |
+
assert result['default_speed'] == 1.0
|
133 |
+
mock_config.get_tts_config.assert_called_once()
|
134 |
+
|
135 |
+
def test_get_tts_configuration_failure(self, service, mock_config):
|
136 |
+
"""Test TTS configuration retrieval failure"""
|
137 |
+
mock_config.get_tts_config.side_effect = Exception("TTS config error")
|
138 |
+
|
139 |
+
with pytest.raises(ConfigurationException, match="Failed to retrieve TTS configuration"):
|
140 |
+
service.get_tts_configuration()
|
141 |
+
|
142 |
+
def test_get_stt_configuration_success(self, service, mock_config):
|
143 |
+
"""Test successful STT configuration retrieval"""
|
144 |
+
result = service.get_stt_configuration()
|
145 |
+
|
146 |
+
assert result['preferred_providers'] == ['whisper', 'parakeet']
|
147 |
+
assert result['default_model'] == 'whisper'
|
148 |
+
mock_config.get_stt_config.assert_called_once()
|
149 |
+
|
150 |
+
def test_get_stt_configuration_failure(self, service, mock_config):
|
151 |
+
"""Test STT configuration retrieval failure"""
|
152 |
+
mock_config.get_stt_config.side_effect = Exception("STT config error")
|
153 |
+
|
154 |
+
with pytest.raises(ConfigurationException, match="Failed to retrieve STT configuration"):
|
155 |
+
service.get_stt_configuration()
|
156 |
+
|
157 |
+
def test_get_translation_configuration_success(self, service, mock_config):
|
158 |
+
"""Test successful translation configuration retrieval"""
|
159 |
+
result = service.get_translation_configuration()
|
160 |
+
|
161 |
+
assert result['default_provider'] == 'nllb'
|
162 |
+
assert result['model_name'] == 'nllb-200-3.3B'
|
163 |
+
mock_config.get_translation_config.assert_called_once()
|
164 |
+
|
165 |
+
def test_get_translation_configuration_failure(self, service, mock_config):
|
166 |
+
"""Test translation configuration retrieval failure"""
|
167 |
+
mock_config.get_translation_config.side_effect = Exception("Translation config error")
|
168 |
+
|
169 |
+
with pytest.raises(ConfigurationException, match="Failed to retrieve translation configuration"):
|
170 |
+
service.get_translation_configuration()
|
171 |
+
|
172 |
+
def test_get_processing_configuration_success(self, service, mock_config):
|
173 |
+
"""Test successful processing configuration retrieval"""
|
174 |
+
result = service.get_processing_configuration()
|
175 |
+
|
176 |
+
assert result['temp_dir'] == '/tmp'
|
177 |
+
assert result['max_file_size_mb'] == 100
|
178 |
+
mock_config.get_processing_config.assert_called_once()
|
179 |
+
|
180 |
+
def test_get_processing_configuration_failure(self, service, mock_config):
|
181 |
+
"""Test processing configuration retrieval failure"""
|
182 |
+
mock_config.get_processing_config.side_effect = Exception("Processing config error")
|
183 |
+
|
184 |
+
with pytest.raises(ConfigurationException, match="Failed to retrieve processing configuration"):
|
185 |
+
service.get_processing_configuration()
|
186 |
+
|
187 |
+
def test_get_logging_configuration_success(self, service, mock_config):
|
188 |
+
"""Test successful logging configuration retrieval"""
|
189 |
+
result = service.get_logging_configuration()
|
190 |
+
|
191 |
+
assert result['level'] == 'INFO'
|
192 |
+
assert result['enable_file_logging'] is False
|
193 |
+
mock_config.get_logging_config.assert_called_once()
|
194 |
+
|
195 |
+
def test_get_logging_configuration_failure(self, service, mock_config):
|
196 |
+
"""Test logging configuration retrieval failure"""
|
197 |
+
mock_config.get_logging_config.side_effect = Exception("Logging config error")
|
198 |
+
|
199 |
+
with pytest.raises(ConfigurationException, match="Failed to retrieve logging configuration"):
|
200 |
+
service.get_logging_configuration()
|
201 |
+
|
202 |
+
def test_update_tts_configuration_success(self, service, mock_config):
|
203 |
+
"""Test successful TTS configuration update"""
|
204 |
+
updates = {
|
205 |
+
'default_speed': 1.5,
|
206 |
+
'enable_streaming': True
|
207 |
+
}
|
208 |
+
|
209 |
+
result = service.update_tts_configuration(updates)
|
210 |
+
|
211 |
+
# Verify setattr was called for valid attributes
|
212 |
+
assert hasattr(mock_config.tts, 'default_speed')
|
213 |
+
assert hasattr(mock_config.tts, 'enable_streaming')
|
214 |
+
|
215 |
+
# Verify updated config was returned
|
216 |
+
mock_config.get_tts_config.assert_called()
|
217 |
+
|
218 |
+
def test_update_tts_configuration_validation_error(self, service):
|
219 |
+
"""Test TTS configuration update with validation error"""
|
220 |
+
updates = {
|
221 |
+
'default_speed': 5.0 # Invalid speed > 3.0
|
222 |
+
}
|
223 |
+
|
224 |
+
with pytest.raises(ConfigurationException, match="default_speed must be between 0.1 and 3.0"):
|
225 |
+
service.update_tts_configuration(updates)
|
226 |
+
|
227 |
+
def test_update_stt_configuration_success(self, service, mock_config):
|
228 |
+
"""Test successful STT configuration update"""
|
229 |
+
updates = {
|
230 |
+
'chunk_length_s': 60,
|
231 |
+
'enable_vad': False
|
232 |
+
}
|
233 |
+
|
234 |
+
result = service.update_stt_configuration(updates)
|
235 |
+
|
236 |
+
# Verify setattr was called for valid attributes
|
237 |
+
assert hasattr(mock_config.stt, 'chunk_length_s')
|
238 |
+
assert hasattr(mock_config.stt, 'enable_vad')
|
239 |
+
|
240 |
+
# Verify updated config was returned
|
241 |
+
mock_config.get_stt_config.assert_called()
|
242 |
+
|
243 |
+
def test_update_stt_configuration_validation_error(self, service):
|
244 |
+
"""Test STT configuration update with validation error"""
|
245 |
+
updates = {
|
246 |
+
'chunk_length_s': -10 # Invalid negative value
|
247 |
+
}
|
248 |
+
|
249 |
+
with pytest.raises(ConfigurationException, match="chunk_length_s must be a positive integer"):
|
250 |
+
service.update_stt_configuration(updates)
|
251 |
+
|
252 |
+
def test_update_translation_configuration_success(self, service, mock_config):
|
253 |
+
"""Test successful translation configuration update"""
|
254 |
+
updates = {
|
255 |
+
'max_chunk_length': 1024,
|
256 |
+
'cache_translations': False
|
257 |
+
}
|
258 |
+
|
259 |
+
result = service.update_translation_configuration(updates)
|
260 |
+
|
261 |
+
# Verify setattr was called for valid attributes
|
262 |
+
assert hasattr(mock_config.translation, 'max_chunk_length')
|
263 |
+
assert hasattr(mock_config.translation, 'cache_translations')
|
264 |
+
|
265 |
+
# Verify updated config was returned
|
266 |
+
mock_config.get_translation_config.assert_called()
|
267 |
+
|
268 |
+
def test_update_translation_configuration_validation_error(self, service):
|
269 |
+
"""Test translation configuration update with validation error"""
|
270 |
+
updates = {
|
271 |
+
'max_chunk_length': 0 # Invalid zero value
|
272 |
+
}
|
273 |
+
|
274 |
+
with pytest.raises(ConfigurationException, match="max_chunk_length must be a positive integer"):
|
275 |
+
service.update_translation_configuration(updates)
|
276 |
+
|
277 |
+
def test_update_processing_configuration_success(self, service, mock_config):
|
278 |
+
"""Test successful processing configuration update"""
|
279 |
+
updates = {
|
280 |
+
'max_file_size_mb': 200,
|
281 |
+
'cleanup_temp_files': False
|
282 |
+
}
|
283 |
+
|
284 |
+
with patch('pathlib.Path.mkdir'):
|
285 |
+
result = service.update_processing_configuration(updates)
|
286 |
+
|
287 |
+
# Verify setattr was called for valid attributes
|
288 |
+
assert hasattr(mock_config.processing, 'max_file_size_mb')
|
289 |
+
assert hasattr(mock_config.processing, 'cleanup_temp_files')
|
290 |
+
|
291 |
+
# Verify updated config was returned
|
292 |
+
mock_config.get_processing_config.assert_called()
|
293 |
+
|
294 |
+
def test_update_processing_configuration_validation_error(self, service):
|
295 |
+
"""Test processing configuration update with validation error"""
|
296 |
+
updates = {
|
297 |
+
'max_file_size_mb': -50 # Invalid negative value
|
298 |
+
}
|
299 |
+
|
300 |
+
with pytest.raises(ConfigurationException, match="max_file_size_mb must be a positive integer"):
|
301 |
+
service.update_processing_configuration(updates)
|
302 |
+
|
303 |
+
def test_validate_tts_updates_valid(self, service):
|
304 |
+
"""Test TTS updates validation with valid data"""
|
305 |
+
updates = {
|
306 |
+
'preferred_providers': ['kokoro', 'dia'],
|
307 |
+
'default_speed': 1.5,
|
308 |
+
'default_language': 'es',
|
309 |
+
'enable_streaming': True,
|
310 |
+
'max_text_length': 10000
|
311 |
+
}
|
312 |
+
|
313 |
+
# Should not raise exception
|
314 |
+
service._validate_tts_updates(updates)
|
315 |
+
|
316 |
+
def test_validate_tts_updates_invalid_provider(self, service):
|
317 |
+
"""Test TTS updates validation with invalid provider"""
|
318 |
+
updates = {
|
319 |
+
'preferred_providers': ['invalid_provider']
|
320 |
+
}
|
321 |
+
|
322 |
+
with pytest.raises(ConfigurationException, match="Invalid TTS provider"):
|
323 |
+
service._validate_tts_updates(updates)
|
324 |
+
|
325 |
+
def test_validate_tts_updates_invalid_speed(self, service):
|
326 |
+
"""Test TTS updates validation with invalid speed"""
|
327 |
+
updates = {
|
328 |
+
'default_speed': 5.0 # Too high
|
329 |
+
}
|
330 |
+
|
331 |
+
with pytest.raises(ConfigurationException, match="default_speed must be between 0.1 and 3.0"):
|
332 |
+
service._validate_tts_updates(updates)
|
333 |
+
|
334 |
+
def test_validate_stt_updates_valid(self, service):
|
335 |
+
"""Test STT updates validation with valid data"""
|
336 |
+
updates = {
|
337 |
+
'preferred_providers': ['whisper', 'parakeet'],
|
338 |
+
'default_model': 'whisper',
|
339 |
+
'chunk_length_s': 45,
|
340 |
+
'batch_size': 32,
|
341 |
+
'enable_vad': False
|
342 |
+
}
|
343 |
+
|
344 |
+
# Should not raise exception
|
345 |
+
service._validate_stt_updates(updates)
|
346 |
+
|
347 |
+
def test_validate_stt_updates_invalid_provider(self, service):
|
348 |
+
"""Test STT updates validation with invalid provider"""
|
349 |
+
updates = {
|
350 |
+
'preferred_providers': ['invalid_stt']
|
351 |
+
}
|
352 |
+
|
353 |
+
with pytest.raises(ConfigurationException, match="Invalid STT provider"):
|
354 |
+
service._validate_stt_updates(updates)
|
355 |
+
|
356 |
+
def test_validate_translation_updates_valid(self, service):
|
357 |
+
"""Test translation updates validation with valid data"""
|
358 |
+
updates = {
|
359 |
+
'default_provider': 'nllb',
|
360 |
+
'model_name': 'nllb-200-1.3B',
|
361 |
+
'max_chunk_length': 256,
|
362 |
+
'batch_size': 4,
|
363 |
+
'cache_translations': False
|
364 |
+
}
|
365 |
+
|
366 |
+
# Should not raise exception
|
367 |
+
service._validate_translation_updates(updates)
|
368 |
+
|
369 |
+
def test_validate_processing_updates_valid(self, service):
|
370 |
+
"""Test processing updates validation with valid data"""
|
371 |
+
updates = {
|
372 |
+
'temp_dir': '/tmp/test',
|
373 |
+
'cleanup_temp_files': True,
|
374 |
+
'max_file_size_mb': 150,
|
375 |
+
'supported_audio_formats': ['wav', 'mp3'],
|
376 |
+
'processing_timeout_seconds': 600
|
377 |
+
}
|
378 |
+
|
379 |
+
with patch('pathlib.Path.mkdir'):
|
380 |
+
# Should not raise exception
|
381 |
+
service._validate_processing_updates(updates)
|
382 |
+
|
383 |
+
def test_validate_processing_updates_invalid_format(self, service):
|
384 |
+
"""Test processing updates validation with invalid audio format"""
|
385 |
+
updates = {
|
386 |
+
'supported_audio_formats': ['wav', 'invalid_format']
|
387 |
+
}
|
388 |
+
|
389 |
+
with pytest.raises(ConfigurationException, match="Invalid audio format"):
|
390 |
+
service._validate_processing_updates(updates)
|
391 |
+
|
392 |
+
def test_save_configuration_to_file_success(self, service, mock_config):
|
393 |
+
"""Test successful configuration save to file"""
|
394 |
+
file_path = "/tmp/config.json"
|
395 |
+
|
396 |
+
service.save_configuration_to_file(file_path)
|
397 |
+
|
398 |
+
mock_config.save_configuration.assert_called_once_with(file_path)
|
399 |
+
|
400 |
+
def test_save_configuration_to_file_failure(self, service, mock_config):
|
401 |
+
"""Test configuration save to file failure"""
|
402 |
+
file_path = "/tmp/config.json"
|
403 |
+
mock_config.save_configuration.side_effect = Exception("Save failed")
|
404 |
+
|
405 |
+
with pytest.raises(ConfigurationException, match="Failed to save configuration"):
|
406 |
+
service.save_configuration_to_file(file_path)
|
407 |
+
|
408 |
+
@patch('os.path.exists')
|
409 |
+
def test_load_configuration_from_file_success(self, mock_exists, service, mock_container):
|
410 |
+
"""Test successful configuration load from file"""
|
411 |
+
file_path = "/tmp/config.json"
|
412 |
+
mock_exists.return_value = True
|
413 |
+
|
414 |
+
with patch('src.infrastructure.config.app_config.AppConfig') as mock_app_config:
|
415 |
+
new_config = Mock()
|
416 |
+
mock_app_config.return_value = new_config
|
417 |
+
|
418 |
+
result = service.load_configuration_from_file(file_path)
|
419 |
+
|
420 |
+
# Verify new config was created and registered
|
421 |
+
mock_app_config.assert_called_once_with(config_file=file_path)
|
422 |
+
mock_container.register_singleton.assert_called_once_with(AppConfig, new_config)
|
423 |
+
|
424 |
+
@patch('os.path.exists')
|
425 |
+
def test_load_configuration_from_file_not_found(self, mock_exists, service):
|
426 |
+
"""Test configuration load from non-existent file"""
|
427 |
+
file_path = "/tmp/nonexistent.json"
|
428 |
+
mock_exists.return_value = False
|
429 |
+
|
430 |
+
with pytest.raises(ConfigurationException, match="Configuration file not found"):
|
431 |
+
service.load_configuration_from_file(file_path)
|
432 |
+
|
433 |
+
def test_reload_configuration_success(self, service, mock_config):
|
434 |
+
"""Test successful configuration reload"""
|
435 |
+
result = service.reload_configuration()
|
436 |
+
|
437 |
+
mock_config.reload_configuration.assert_called_once()
|
438 |
+
assert 'tts' in result
|
439 |
+
|
440 |
+
def test_reload_configuration_failure(self, service, mock_config):
|
441 |
+
"""Test configuration reload failure"""
|
442 |
+
mock_config.reload_configuration.side_effect = Exception("Reload failed")
|
443 |
+
|
444 |
+
with pytest.raises(ConfigurationException, match="Failed to reload configuration"):
|
445 |
+
service.reload_configuration()
|
446 |
+
|
447 |
+
def test_get_provider_availability(self, service, mock_container):
|
448 |
+
"""Test provider availability check"""
|
449 |
+
# Mock factories
|
450 |
+
mock_tts_factory = Mock()
|
451 |
+
mock_stt_factory = Mock()
|
452 |
+
mock_translation_factory = Mock()
|
453 |
+
|
454 |
+
mock_container.resolve.side_effect = [mock_tts_factory, mock_stt_factory, mock_translation_factory]
|
455 |
+
|
456 |
+
# Mock successful provider creation
|
457 |
+
mock_tts_factory.create_provider.return_value = Mock()
|
458 |
+
mock_stt_factory.create_provider.return_value = Mock()
|
459 |
+
mock_translation_factory.get_default_provider.return_value = Mock()
|
460 |
+
|
461 |
+
result = service.get_provider_availability()
|
462 |
+
|
463 |
+
assert 'tts' in result
|
464 |
+
assert 'stt' in result
|
465 |
+
assert 'translation' in result
|
466 |
+
|
467 |
+
# All providers should be available
|
468 |
+
assert all(result['tts'].values())
|
469 |
+
assert all(result['stt'].values())
|
470 |
+
assert result['translation']['nllb'] is True
|
471 |
+
|
472 |
+
def test_get_system_info(self, service, mock_config):
|
473 |
+
"""Test system information retrieval"""
|
474 |
+
mock_config.config_file = "/tmp/config.json"
|
475 |
+
mock_config.processing.temp_dir = "/tmp"
|
476 |
+
mock_config.logging.level = "INFO"
|
477 |
+
mock_config.processing.supported_audio_formats = ['wav', 'mp3']
|
478 |
+
mock_config.processing.max_file_size_mb = 100
|
479 |
+
mock_config.processing.processing_timeout_seconds = 300
|
480 |
+
|
481 |
+
with patch.object(service, 'get_provider_availability', return_value={}):
|
482 |
+
result = service.get_system_info()
|
483 |
+
|
484 |
+
assert result['config_file'] == "/tmp/config.json"
|
485 |
+
assert result['temp_directory'] == "/tmp"
|
486 |
+
assert result['log_level'] == "INFO"
|
487 |
+
assert 'supported_languages' in result
|
488 |
+
assert 'supported_audio_formats' in result
|
489 |
+
assert 'max_file_size_mb' in result
|
490 |
+
|
491 |
+
def test_validate_configuration_success(self, service, mock_config):
|
492 |
+
"""Test successful configuration validation"""
|
493 |
+
# Mock valid configuration
|
494 |
+
mock_config.get_tts_config.return_value = {
|
495 |
+
'default_speed': 1.0,
|
496 |
+
'max_text_length': 5000
|
497 |
+
}
|
498 |
+
mock_config.get_stt_config.return_value = {
|
499 |
+
'chunk_length_s': 30,
|
500 |
+
'batch_size': 16
|
501 |
+
}
|
502 |
+
mock_config.get_processing_config.return_value = {
|
503 |
+
'temp_dir': '/tmp',
|
504 |
+
'max_file_size_mb': 100
|
505 |
+
}
|
506 |
+
mock_config.get_logging_config.return_value = {
|
507 |
+
'level': 'INFO'
|
508 |
+
}
|
509 |
+
|
510 |
+
with patch('os.path.exists', return_value=True):
|
511 |
+
result = service.validate_configuration()
|
512 |
+
|
513 |
+
# Should have no issues
|
514 |
+
assert all(len(issues) == 0 for issues in result.values())
|
515 |
+
|
516 |
+
def test_validate_configuration_with_issues(self, service, mock_config):
|
517 |
+
"""Test configuration validation with issues"""
|
518 |
+
# Mock invalid configuration
|
519 |
+
mock_config.get_tts_config.return_value = {
|
520 |
+
'default_speed': 5.0, # Invalid
|
521 |
+
'max_text_length': -100 # Invalid
|
522 |
+
}
|
523 |
+
mock_config.get_stt_config.return_value = {
|
524 |
+
'chunk_length_s': -10, # Invalid
|
525 |
+
'batch_size': 0 # Invalid
|
526 |
+
}
|
527 |
+
mock_config.get_processing_config.return_value = {
|
528 |
+
'temp_dir': '/nonexistent', # Invalid
|
529 |
+
'max_file_size_mb': -50 # Invalid
|
530 |
+
}
|
531 |
+
mock_config.get_logging_config.return_value = {
|
532 |
+
'level': 'INVALID' # Invalid
|
533 |
+
}
|
534 |
+
|
535 |
+
with patch('os.path.exists', return_value=False):
|
536 |
+
result = service.validate_configuration()
|
537 |
+
|
538 |
+
# Should have issues in each category
|
539 |
+
assert len(result['tts']) > 0
|
540 |
+
assert len(result['stt']) > 0
|
541 |
+
assert len(result['processing']) > 0
|
542 |
+
assert len(result['logging']) > 0
|
543 |
+
|
544 |
+
def test_reset_to_defaults_success(self, service, mock_container):
|
545 |
+
"""Test successful configuration reset to defaults"""
|
546 |
+
with patch('src.infrastructure.config.app_config.AppConfig') as mock_app_config:
|
547 |
+
default_config = Mock()
|
548 |
+
mock_app_config.return_value = default_config
|
549 |
+
|
550 |
+
result = service.reset_to_defaults()
|
551 |
+
|
552 |
+
# Verify new default config was created and registered
|
553 |
+
mock_app_config.assert_called_once_with()
|
554 |
+
mock_container.register_singleton.assert_called_once_with(AppConfig, default_config)
|
555 |
+
|
556 |
+
def test_reset_to_defaults_failure(self, service):
|
557 |
+
"""Test configuration reset to defaults failure"""
|
558 |
+
with patch('src.infrastructure.config.app_config.AppConfig', side_effect=Exception("Reset failed")):
|
559 |
+
with pytest.raises(ConfigurationException, match="Failed to reset configuration"):
|
560 |
+
service.reset_to_defaults()
|
561 |
+
|
562 |
+
def test_cleanup(self, service):
|
563 |
+
"""Test service cleanup"""
|
564 |
+
# Should not raise exception
|
565 |
+
service.cleanup()
|
566 |
+
|
567 |
+
def test_context_manager(self, service):
|
568 |
+
"""Test service as context manager"""
|
569 |
+
with patch.object(service, 'cleanup') as mock_cleanup:
|
570 |
+
with service as svc:
|
571 |
+
assert svc == service
|
572 |
+
mock_cleanup.assert_called_once()
|