nifty-coder/stemsplit-backend
0
1"""2Integration tests for Azure Speech Services provider.3 4This module contains integration tests that verify the AzureSpeechProvider5works correctly with the broader voice control system, including provider6manager integration, error handling, and real-world scenarios.7"""8 9import pytest10import asyncio11import json12from unittest.mock import Mock, AsyncMock, patch, MagicMock13from datetime import datetime, timezone, timedelta14from typing import Dict, Any, List15 16from voice_control.providers.azure_speech_provider import AzureSpeechProvider, create_azure_speech_provider17from voice_control.models import (18 ProviderConfig, 19 TranscriptionResult, 20 QuotaStatus, 21 QuotaType,22 ProviderType,23 ProviderStatus24)25from voice_control.exceptions import (26 ProviderError,27 ProviderUnavailableError,28 ProviderAuthenticationError,29 ProviderTimeoutError,30 UnsupportedFormatError,31 UnsupportedLanguageError,32 QuotaExceededError,33 RateLimitExceededError,34 TranscriptionError35)36 37 38class TestAzureSpeechIntegration:39 """Integration tests for Azure Speech provider with system components."""40 41 @pytest.fixture42 def mock_azure_sdk(self):43 """Mock Azure Speech SDK for integration tests."""44 with patch('voice_control.providers.azure_speech_provider.AZURE_SPEECH_AVAILABLE', True):45 with patch('voice_control.providers.azure_speech_provider.speechsdk') as mock_sdk:46 # Mock all necessary components47 mock_speech_config = Mock()48 mock_sdk.SpeechConfig.return_value = mock_speech_config49 50 mock_audio_config = Mock()51 mock_sdk.audio.AudioConfig.return_value = mock_audio_config52 53 mock_push_stream = Mock()54 mock_sdk.audio.PushAudioInputStream.return_value = mock_push_stream55 56 mock_recognizer = Mock()57 mock_sdk.SpeechRecognizer.return_value = mock_recognizer58 59 # Mock enums60 mock_sdk.ResultReason.RecognizedSpeech = "RecognizedSpeech"61 mock_sdk.ResultReason.NoMatch = "NoMatch"62 mock_sdk.ResultReason.RecognizingSpeech = "RecognizingSpeech"63 mock_sdk.CancellationReason.Error = "Error"64 mock_sdk.OutputFormat.Detailed = "Detailed"65 mock_sdk.ProfanityOption.Raw = "Raw"66 mock_sdk.ProfanityOption.Masked = "Masked"67 68 yield mock_sdk69 70 @pytest.fixture71 def provider_config(self):72 """Create provider configuration for integration tests."""73 return ProviderConfig(74 name="azure_integration",75 provider_type=ProviderType.AZURE_SPEECH,76 enabled=True,77 priority=3,78 free_tier_limits={79 "audio_hours_per_month": 5.0,80 "requests_per_minute": 20,81 "requests_per_day": 500082 },83 rate_limits={84 "requests_per_minute": 20,85 "requests_per_day": 500086 },87 supported_formats=["wav", "flac", "mp3", "ogg", "webm"],88 supported_languages=["en-US", "en-GB", "es-ES", "fr-FR", "de-DE"],89 cost_per_minute=2.5 / 60.0,90 api_credentials={91 "subscription_key": "integration_test_key_12345",92 "region": "eastus"93 },94 timeout_seconds=60,95 max_retries=396 )97 98 @pytest.fixture99 def provider(self, provider_config, mock_azure_sdk):100 """Create Azure Speech provider for integration tests."""101 return AzureSpeechProvider(provider_config)102 103 @pytest.mark.asyncio104 async def test_provider_manager_integration(self, provider, mock_azure_sdk):105 """Test integration with provider manager system."""106 # Mock successful transcription107 mock_result = Mock()108 mock_result.reason = "RecognizedSpeech"109 mock_result.text = "Provider manager integration test"110 mock_result.json = json.dumps({111 "NBest": [{112 "Confidence": 0.88,113 "Display": "Provider manager integration test",114 "Words": [115 {"Word": "Provider", "Offset": 0, "Duration": 5000000, "Confidence": 0.9},116 {"Word": "manager", "Offset": 5000000, "Duration": 5000000, "Confidence": 0.85},117 {"Word": "integration", "Offset": 10000000, "Duration": 8000000, "Confidence": 0.9},118 {"Word": "test", "Offset": 18000000, "Duration": 4000000, "Confidence": 0.87}119 ]120 }]121 })122 123 mock_recognizer = mock_azure_sdk.SpeechRecognizer.return_value124 mock_recognizer.recognize_once.return_value = mock_result125 126 # Test transcription through provider interface127 audio_data = b"provider_manager_test_audio"128 result = await provider.transcribe_audio(129 audio_data, 130 "wav", 131 "en-US",132 enable_word_timestamps=True133 )134 135 # Verify provider manager compatible result136 assert isinstance(result, TranscriptionResult)137 assert result.text == "Provider manager integration test"138 assert result.confidence == 0.88139 assert result.provider == "azure_integration"140 assert result.language == "en-US"141 assert result.is_final142 assert len(result.word_timestamps) == 4143 144 # Verify word timestamps are properly formatted145 assert result.word_timestamps[0].word == "Provider"146 assert result.word_timestamps[0].start_time == 0.0147 assert result.word_timestamps[0].end_time == 0.5148 assert result.word_timestamps[0].confidence == 0.9149 150 @pytest.mark.asyncio151 async def test_fallback_chain_integration(self, provider, mock_azure_sdk):152 """Test provider behavior in fallback chain scenarios."""153 # Test 1: Provider available and working154 mock_result = Mock()155 mock_result.reason = "RecognizedSpeech"156 mock_result.text = "Fallback test success"157 mock_result.json = json.dumps({158 "NBest": [{"Confidence": 0.92, "Display": "Fallback test success"}]159 })160 161 mock_recognizer = mock_azure_sdk.SpeechRecognizer.return_value162 mock_recognizer.recognize_once.return_value = mock_result163 164 # Should work normally165 health = await provider.check_health()166 assert health is True167 168 result = await provider.transcribe_audio(b"test_audio", "wav", "en-US")169 assert result.text == "Fallback test success"170 171 # Test 2: Provider failure (should raise appropriate exception for fallback)172 mock_recognizer.recognize_once.side_effect = Exception("Service temporarily unavailable")173 174 with pytest.raises(TranscriptionError):175 await provider.transcribe_audio(b"test_audio", "wav", "en-US")176 177 # Health check should also fail178 health = await provider.check_health()179 assert health is False180 181 @pytest.mark.asyncio182 async def test_rate_limiter_integration(self, provider, mock_azure_sdk):183 """Test integration with rate limiting system."""184 # Mock successful transcription185 mock_result = Mock()186 mock_result.reason = "RecognizedSpeech"187 mock_result.text = "Rate limit test"188 mock_result.json = json.dumps({189 "NBest": [{"Confidence": 0.9, "Display": "Rate limit test"}]190 })191 192 mock_recognizer = mock_azure_sdk.SpeechRecognizer.return_value193 mock_recognizer.recognize_once.return_value = mock_result194 195 # Test quota checking before requests196 quota_status = await provider.get_quota_status()197 initial_minute_usage = quota_status["requests_per_minute"].current_usage198 initial_daily_usage = quota_status["requests_per_day"].current_usage199 200 # Make a request201 result = await provider.transcribe_audio(b"test_audio", "wav", "en-US")202 assert result.text == "Rate limit test"203 204 # Verify quota was consumed205 quota_status = await provider.get_quota_status()206 assert quota_status["requests_per_minute"].current_usage == initial_minute_usage + 1207 assert quota_status["requests_per_day"].current_usage == initial_daily_usage + 1208 209 # Test rate limit enforcement210 provider._usage_stats["requests_this_minute"] = 20 # At limit211 212 with pytest.raises(RateLimitExceededError) as exc_info:213 await provider.transcribe_audio(b"test_audio", "wav", "en-US")214 215 assert exc_info.value.provider == "azure_integration"216 assert exc_info.value.retry_after == 60.0217 218 @pytest.mark.asyncio219 async def test_audio_processor_integration(self, provider, mock_azure_sdk):220 """Test integration with audio processing components."""221 # Mock successful transcription222 mock_result = Mock()223 mock_result.reason = "RecognizedSpeech"224 mock_result.text = "Audio processing integration test"225 mock_result.json = json.dumps({226 "NBest": [{"Confidence": 0.87, "Display": "Audio processing integration test"}]227 })228 229 mock_recognizer = mock_azure_sdk.SpeechRecognizer.return_value230 mock_recognizer.recognize_once.return_value = mock_result231 232 # Test different audio formats233 test_formats = ["wav", "mp3", "flac", "webm"]234 235 for format_type in test_formats:236 if provider.supports_format(format_type):237 audio_data = b"test_audio_" + format_type.encode()238 239 result = await provider.transcribe_audio(240 audio_data, 241 format_type, 242 "en-US",243 audio_duration=2.5 # Provide duration for testing244 )245 246 assert result.text == "Audio processing integration test"247 assert result.audio_duration == 2.5248 249 # Verify format-specific processing250 mock_azure_sdk.SpeechConfig.assert_called()251 mock_azure_sdk.SpeechRecognizer.assert_called()252 253 @pytest.mark.asyncio254 async def test_cache_manager_integration(self, provider, mock_azure_sdk):255 """Test integration with caching system."""256 # Mock successful transcription257 mock_result = Mock()258 mock_result.reason = "RecognizedSpeech"259 mock_result.text = "Cache integration test"260 mock_result.json = json.dumps({261 "NBest": [{"Confidence": 0.91, "Display": "Cache integration test"}]262 })263 264 mock_recognizer = mock_azure_sdk.SpeechRecognizer.return_value265 mock_recognizer.recognize_once.return_value = mock_result266 267 # Test transcription (should be cacheable)268 audio_data = b"cacheable_audio_data"269 270 result1 = await provider.transcribe_audio(audio_data, "wav", "en-US")271 assert result1.text == "Cache integration test"272 273 # Second request with same audio (cache system would handle this)274 result2 = await provider.transcribe_audio(audio_data, "wav", "en-US")275 assert result2.text == "Cache integration test"276 277 # Both results should have consistent metadata for caching278 assert result1.provider == result2.provider279 assert result1.language == result2.language280 assert result1.confidence == result2.confidence281 282 @pytest.mark.asyncio283 async def test_cost_monitor_integration(self, provider, mock_azure_sdk):284 """Test integration with cost monitoring system."""285 # Mock successful transcription286 mock_result = Mock()287 mock_result.reason = "RecognizedSpeech"288 mock_result.text = "Cost monitoring test"289 mock_result.json = json.dumps({290 "NBest": [{"Confidence": 0.89, "Display": "Cost monitoring test"}]291 })292 293 mock_recognizer = mock_azure_sdk.SpeechRecognizer.return_value294 mock_recognizer.recognize_once.return_value = mock_result295 296 # Test cost estimation297 audio_duration = 3600.0 # 1 hour298 estimated_cost = await provider.estimate_cost(audio_duration)299 300 # Should be free within 5-hour limit301 assert estimated_cost == 0.0302 303 # Test cost for exceeding free tier304 audio_duration = 6 * 3600.0 # 6 hours305 estimated_cost = await provider.estimate_cost(audio_duration)306 307 # Should charge for 1 hour at neural pricing308 expected_cost = 1 * 2.5 # 1 hour * $2.50/hour309 assert estimated_cost == expected_cost310 311 # Test actual usage tracking312 initial_usage = provider._usage_stats["audio_hours_this_month"]313 314 result = await provider.transcribe_audio(315 b"cost_test_audio", 316 "wav", 317 "en-US",318 audio_duration=1800.0 # 30 minutes319 )320 321 # Verify usage was tracked322 final_usage = provider._usage_stats["audio_hours_this_month"]323 assert final_usage > initial_usage324 assert final_usage == initial_usage + 0.5 # 30 minutes = 0.5 hours325 326 @pytest.mark.asyncio327 async def test_streaming_integration(self, provider, mock_azure_sdk):328 """Test streaming transcription integration."""329 # Mock streaming components330 mock_push_stream = Mock()331 mock_azure_sdk.audio.PushAudioInputStream.return_value = mock_push_stream332 333 mock_recognizer = mock_azure_sdk.SpeechRecognizer.return_value334 mock_recognizer.recognized = Mock()335 mock_recognizer.recognizing = Mock()336 mock_recognizer.start_continuous_recognition = Mock()337 mock_recognizer.stop_continuous_recognition = Mock()338 339 # Create test audio stream340 async def test_audio_stream():341 chunks = [342 b"chunk_1_audio_data",343 b"chunk_2_audio_data", 344 b"chunk_3_audio_data",345 b"final_chunk_audio"346 ]347 for chunk in chunks:348 yield chunk349 await asyncio.sleep(0.1) # Simulate streaming delay350 351 # Test streaming transcription352 session_id = "test_streaming_session"353 results = []354 355 async for result in provider.transcribe_streaming(356 test_audio_stream(),357 "wav",358 "en-US",359 session_id=session_id,360 interim_results=True361 ):362 results.append(result)363 364 # Verify streaming setup was called365 mock_push_stream.write.assert_called()366 mock_push_stream.close.assert_called_once()367 mock_recognizer.start_continuous_recognition.assert_called_once()368 mock_recognizer.stop_continuous_recognition.assert_called_once()369 370 # Verify session was tracked371 assert session_id not in provider._active_streams # Should be cleaned up372 373 @pytest.mark.asyncio374 async def test_error_handling_integration(self, provider, mock_azure_sdk):375 """Test comprehensive error handling integration."""376 # Test authentication error377 mock_recognizer = mock_azure_sdk.SpeechRecognizer.return_value378 mock_recognizer.recognize_once.side_effect = Exception("unauthorized access denied")379 380 with pytest.raises(ProviderAuthenticationError):381 await provider.transcribe_audio(b"test_audio", "wav", "en-US")382 383 # Test rate limit error384 mock_recognizer.recognize_once.side_effect = Exception("rate limit exceeded, please try again")385 386 with pytest.raises(RateLimitExceededError):387 await provider.transcribe_audio(b"test_audio", "wav", "en-US")388 389 # Test timeout error390 mock_recognizer.recognize_once.side_effect = Exception("request timed out after 60 seconds")391 392 with pytest.raises(ProviderTimeoutError):393 await provider.transcribe_audio(b"test_audio", "wav", "en-US")394 395 # Test service unavailable error396 mock_recognizer.recognize_once.side_effect = Exception("service temporarily unavailable")397 398 with pytest.raises(ProviderUnavailableError):399 await provider.transcribe_audio(b"test_audio", "wav", "en-US")400 401 # Test generic error402 mock_recognizer.recognize_once.side_effect = Exception("unknown internal error")403 404 with pytest.raises(TranscriptionError):405 await provider.transcribe_audio(b"test_audio", "wav", "en-US")406 407 @pytest.mark.asyncio408 async def test_multi_language_integration(self, provider, mock_azure_sdk):409 """Test multi-language support integration."""410 # Mock successful transcription for different languages411 mock_result = Mock()412 mock_result.reason = "RecognizedSpeech"413 mock_result.json = json.dumps({414 "NBest": [{"Confidence": 0.85, "Display": "Multi-language test"}]415 })416 417 mock_recognizer = mock_azure_sdk.SpeechRecognizer.return_value418 mock_recognizer.recognize_once.return_value = mock_result419 420 # Test supported languages421 test_languages = ["en-US", "en-GB", "es-ES", "fr-FR", "de-DE"]422 423 for language in test_languages:424 if provider.supports_language(language):425 mock_result.text = f"Test in {language}"426 427 result = await provider.transcribe_audio(428 b"multilingual_test_audio",429 "wav",430 language431 )432 433 assert result.text == f"Test in {language}"434 assert result.language == language435 assert result.confidence == 0.85436 437 # Test unsupported language438 with pytest.raises(UnsupportedLanguageError):439 await provider.transcribe_audio(b"test_audio", "wav", "xx-XX")440 441 @pytest.mark.asyncio442 async def test_quota_reset_integration(self, provider, mock_azure_sdk):443 """Test quota reset timing integration."""444 # Set up initial usage445 provider._usage_stats["requests_this_minute"] = 10446 provider._usage_stats["requests_today"] = 100447 provider._usage_stats["audio_hours_this_month"] = 2.5448 449 # Set timestamps to trigger resets450 now = datetime.now(timezone.utc)451 provider._usage_stats["last_minute_reset"] = now - timedelta(minutes=2)452 provider._usage_stats["last_daily_reset"] = now.date() - timedelta(days=1)453 provider._usage_stats["last_monthly_reset"] = now - timedelta(days=32)454 455 # Check quota status (should trigger resets)456 quota_status = await provider.get_quota_status()457 458 # Verify resets occurred459 assert provider._usage_stats["requests_this_minute"] == 0460 assert provider._usage_stats["requests_today"] == 0461 assert provider._usage_stats["audio_hours_this_month"] == 0.0462 463 # Verify quota status reflects resets464 assert quota_status["requests_per_minute"].current_usage == 0465 assert quota_status["requests_per_day"].current_usage == 0466 assert quota_status["audio_hours_per_month"].current_usage == 0.0467 468 @pytest.mark.asyncio469 async def test_configuration_integration(self, mock_azure_sdk):470 """Test provider configuration integration."""471 # Test with custom endpoint configuration472 custom_config = ProviderConfig(473 name="azure_custom_endpoint",474 provider_type=ProviderType.AZURE_SPEECH,475 enabled=True,476 priority=1,477 free_tier_limits={"audio_hours_per_month": 10.0, "requests_per_minute": 50},478 rate_limits={"requests_per_minute": 50},479 supported_formats=["wav", "mp3"],480 supported_languages=["en-US", "es-ES"],481 cost_per_minute=1.0 / 60.0, # Standard pricing482 api_credentials={483 "subscription_key": "custom_key",484 "endpoint": "https://custom.cognitiveservices.azure.com/"485 },486 timeout_seconds=30,487 max_retries=2488 )489 490 provider = AzureSpeechProvider(custom_config)491 492 # Verify configuration was applied493 assert provider.name == "azure_custom_endpoint"494 assert provider.config.priority == 1495 assert provider.config.free_tier_limits["audio_hours_per_month"] == 10.0496 assert provider.config.api_credentials["endpoint"] == "https://custom.cognitiveservices.azure.com/"497 498 # Test initialization with custom endpoint499 await provider._initialize_client()500 assert provider._client_initialized501 502 # Verify custom limits are used503 quota_status = await provider.get_quota_status()504 assert quota_status["requests_per_minute"].limit == 50505 assert quota_status["audio_hours_per_month"].limit == 10.0506 507 508class TestAzureSpeechProviderFactoryIntegration:509 """Integration tests for Azure Speech provider factory."""510 511 @patch('voice_control.providers.azure_speech_provider.AZURE_SPEECH_AVAILABLE', True)512 def test_factory_integration_with_provider_manager(self):513 """Test factory function integration with provider manager."""514 # Create provider using factory515 provider = create_azure_speech_provider(516 name="factory_test_azure",517 priority=2,518 api_credentials={519 "subscription_key": "factory_test_key",520 "region": "westus2"521 }522 )523 524 # Verify provider is properly configured for provider manager525 assert isinstance(provider, AzureSpeechProvider)526 assert provider.name == "factory_test_azure"527 assert provider.config.priority == 2528 assert provider.config.provider_type == ProviderType.AZURE_SPEECH529 assert provider.config.enabled530 531 # Verify it has all required interface methods532 assert hasattr(provider, 'transcribe_audio')533 assert hasattr(provider, 'transcribe_streaming')534 assert hasattr(provider, 'check_health')535 assert hasattr(provider, 'get_quota_status')536 assert hasattr(provider, 'supports_format')537 assert hasattr(provider, 'supports_language')538 assert hasattr(provider, 'estimate_cost')539 540 # Verify configuration is compatible with system541 assert len(provider.config.supported_formats) > 0542 assert len(provider.config.supported_languages) > 0543 assert provider.config.cost_per_minute >= 0544 assert provider.config.timeout_seconds > 0545 546 547if __name__ == "__main__":548 pytest.main([__file__])