"""Integration tests for Pipeline API endpoints.""" import pytest from fastapi.testclient import TestClient from unittest.mock import Mock, AsyncMock, patch import asyncio from datetime import datetime from backend.main import app from backend.models.pipeline import PipelineStage @pytest.fixture def client(): """Create test client.""" return TestClient(app) @pytest.fixture def mock_dependencies(): """Mock all pipeline dependencies.""" mocks = { 'video_service': Mock(), 'transcript_service': Mock(), 'ai_service': Mock(), 'cache_manager': Mock(), 'notification_service': Mock() } # Setup mock behaviors mocks['video_service'].extract_video_id = AsyncMock(return_value="test_video_id") mocks['video_service'].get_video_metadata = AsyncMock(return_value={ "title": "Test Video", "description": "Test description", "category": "Education" }) mocks['transcript_service'].extract_transcript = AsyncMock() mocks['transcript_service'].extract_transcript.return_value = Mock( transcript="Test transcript content" ) mocks['ai_service'].generate_summary = AsyncMock() mocks['ai_service'].generate_summary.return_value = Mock( summary="Test summary", key_points=["Point 1", "Point 2"], main_themes=["Theme 1"], actionable_insights=["Insight 1"], confidence_score=0.8, processing_metadata={"tokens": 100}, cost_data={"cost": 0.01} ) mocks['cache_manager'].get_cached_pipeline_result = AsyncMock(return_value=None) mocks['cache_manager'].cache_pipeline_result = AsyncMock(return_value=True) mocks['notification_service'].send_completion_notification = AsyncMock(return_value=True) mocks['notification_service'].send_progress_notification = AsyncMock(return_value=True) return mocks class TestPipelineAPI: """Test suite for Pipeline API endpoints.""" def test_process_video_endpoint(self, client): """Test POST /api/process endpoint.""" request_data = { "video_url": "https://youtube.com/watch?v=test123", "summary_length": "standard", "focus_areas": ["main points"], "include_timestamps": False, "enable_notifications": True, "quality_threshold": 0.7 } with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.process_video = AsyncMock(return_value="test_job_id") mock_get_pipeline.return_value = mock_pipeline response = client.post("/api/process", json=request_data) assert response.status_code == 200 data = response.json() assert data["job_id"] == "test_job_id" assert data["status"] == "processing" assert data["message"] == "Video processing started" assert "estimated_completion_time" in data # Verify pipeline was called correctly mock_pipeline.process_video.assert_called_once() def test_process_video_invalid_url(self, client): """Test process endpoint with invalid URL.""" request_data = { "video_url": "not-a-valid-url", "summary_length": "standard" } response = client.post("/api/process", json=request_data) # Should fail validation assert response.status_code == 422 # Validation error def test_process_video_missing_api_key(self, client): """Test process endpoint without API key configured.""" request_data = { "video_url": "https://youtube.com/watch?v=test123" } with patch.dict('os.environ', {}, clear=True): # Clear environment to simulate missing API key response = client.post("/api/process", json=request_data) assert response.status_code == 500 assert "DeepSeek API key not configured" in response.json()["detail"] def test_get_pipeline_status_running(self, client): """Test GET /api/process/{job_id} for running job.""" job_id = "test_job_123" # Mock running pipeline result mock_result = Mock() mock_result.job_id = job_id mock_result.status = PipelineStage.GENERATING_SUMMARY mock_result.video_metadata = {"title": "Test Video"} mock_result.processing_time_seconds = None mock_result.error = None with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.get_pipeline_result = AsyncMock(return_value=mock_result) mock_get_pipeline.return_value = mock_pipeline response = client.get(f"/api/process/{job_id}") assert response.status_code == 200 data = response.json() assert data["job_id"] == job_id assert data["status"] == "generating_summary" assert data["progress_percentage"] == 75 # Based on stage mapping assert "current_message" in data assert data["video_metadata"]["title"] == "Test Video" assert data["result"] is None # Not completed yet def test_get_pipeline_status_completed(self, client): """Test GET /api/process/{job_id} for completed job.""" job_id = "completed_job_456" # Mock completed pipeline result mock_result = Mock() mock_result.job_id = job_id mock_result.status = PipelineStage.COMPLETED mock_result.video_metadata = {"title": "Completed Video"} mock_result.processing_time_seconds = 120.5 mock_result.summary = "Final summary" mock_result.key_points = ["Key point 1", "Key point 2"] mock_result.main_themes = ["Theme 1"] mock_result.actionable_insights = ["Action 1"] mock_result.confidence_score = 0.9 mock_result.quality_score = 0.85 mock_result.cost_data = {"total_cost": 0.05} mock_result.error = None with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.get_pipeline_result = AsyncMock(return_value=mock_result) mock_get_pipeline.return_value = mock_pipeline response = client.get(f"/api/process/{job_id}") assert response.status_code == 200 data = response.json() assert data["job_id"] == job_id assert data["status"] == "completed" assert data["progress_percentage"] == 100 assert data["processing_time_seconds"] == 120.5 # Check result data result = data["result"] assert result["summary"] == "Final summary" assert len(result["key_points"]) == 2 assert len(result["main_themes"]) == 1 assert result["confidence_score"] == 0.9 assert result["quality_score"] == 0.85 def test_get_pipeline_status_failed(self, client): """Test GET /api/process/{job_id} for failed job.""" job_id = "failed_job_789" # Mock failed pipeline result mock_result = Mock() mock_result.job_id = job_id mock_result.status = PipelineStage.FAILED mock_result.video_metadata = None mock_result.processing_time_seconds = 45.0 mock_result.error = { "message": "Video not accessible", "type": "VideoAccessError", "stage": "extracting_transcript", "retry_count": 2 } with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.get_pipeline_result = AsyncMock(return_value=mock_result) mock_get_pipeline.return_value = mock_pipeline response = client.get(f"/api/process/{job_id}") assert response.status_code == 200 data = response.json() assert data["job_id"] == job_id assert data["status"] == "failed" assert data["progress_percentage"] == 0 # Check error data error = data["error"] assert error["message"] == "Video not accessible" assert error["type"] == "VideoAccessError" assert error["retry_count"] == 2 def test_get_pipeline_status_not_found(self, client): """Test GET /api/process/{job_id} for non-existent job.""" job_id = "non_existent_job" with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.get_pipeline_result = AsyncMock(return_value=None) mock_get_pipeline.return_value = mock_pipeline response = client.get(f"/api/process/{job_id}") assert response.status_code == 404 assert "Pipeline job not found" in response.json()["detail"] def test_cancel_pipeline_success(self, client): """Test DELETE /api/process/{job_id} for successful cancellation.""" job_id = "cancel_job_123" with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.cancel_pipeline = AsyncMock(return_value=True) mock_get_pipeline.return_value = mock_pipeline response = client.delete(f"/api/process/{job_id}") assert response.status_code == 200 data = response.json() assert data["message"] == "Pipeline cancelled successfully" mock_pipeline.cancel_pipeline.assert_called_once_with(job_id) def test_cancel_pipeline_not_found(self, client): """Test DELETE /api/process/{job_id} for non-existent job.""" job_id = "non_existent_job" with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.cancel_pipeline = AsyncMock(return_value=False) mock_get_pipeline.return_value = mock_pipeline response = client.delete(f"/api/process/{job_id}") assert response.status_code == 404 assert "Pipeline job not found" in response.json()["detail"] def test_pipeline_history_endpoint(self, client): """Test GET /api/process/{job_id}/history endpoint.""" job_id = "history_job_123" # Mock pipeline result with history mock_result = Mock() mock_result.job_id = job_id mock_result.started_at = datetime(2025, 1, 25, 10, 0, 0) mock_result.completed_at = datetime(2025, 1, 25, 10, 2, 30) mock_result.processing_time_seconds = 150.0 mock_result.retry_count = 1 mock_result.status = PipelineStage.COMPLETED mock_result.video_url = "https://youtube.com/watch?v=test123" mock_result.video_id = "test123" mock_result.error = None with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.get_pipeline_result = AsyncMock(return_value=mock_result) mock_get_pipeline.return_value = mock_pipeline response = client.get(f"/api/process/{job_id}/history") assert response.status_code == 200 data = response.json() assert data["job_id"] == job_id assert data["created_at"] == "2025-01-25T10:00:00" assert data["completed_at"] == "2025-01-25T10:02:30" assert data["processing_time_seconds"] == 150.0 assert data["retry_count"] == 1 assert data["final_status"] == "completed" assert data["video_url"] == "https://youtube.com/watch?v=test123" assert data["error_history"] == [] def test_pipeline_stats_endpoint(self, client): """Test GET /api/stats endpoint.""" with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline, \ patch('backend.api.pipeline.get_cache_manager') as mock_get_cache, \ patch('backend.api.pipeline.get_notification_service') as mock_get_notif, \ patch('backend.api.pipeline.websocket_manager') as mock_ws: # Setup mocks mock_pipeline = Mock() mock_pipeline.get_active_jobs.return_value = ["job1", "job2"] mock_get_pipeline.return_value = mock_pipeline mock_cache = Mock() mock_cache.get_cache_stats = AsyncMock(return_value={ "total_entries": 10, "entries_by_type": {"pipeline_result": 5, "transcript": 3, "metadata": 2} }) mock_get_cache.return_value = mock_cache mock_notif = Mock() mock_notif.get_notification_stats.return_value = { "total_notifications": 25, "notifications_by_type": {"completion": 10, "error": 2, "progress": 13} } mock_get_notif.return_value = mock_notif mock_ws.get_stats.return_value = { "total_connections": 3, "job_connections": {"job1": 2, "job2": 1} } response = client.get("/api/stats") assert response.status_code == 200 data = response.json() assert data["active_jobs"]["count"] == 2 assert data["active_jobs"]["job_ids"] == ["job1", "job2"] assert data["cache"]["total_entries"] == 10 assert data["notifications"]["total_notifications"] == 25 assert data["websockets"]["total_connections"] == 3 assert "timestamp" in data def test_cleanup_endpoint(self, client): """Test POST /api/cleanup endpoint.""" with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline, \ patch('backend.api.pipeline.get_cache_manager') as mock_get_cache, \ patch('backend.api.pipeline.get_notification_service') as mock_get_notif: # Setup mocks mock_pipeline = Mock() mock_pipeline.cleanup_completed_jobs = AsyncMock() mock_get_pipeline.return_value = mock_pipeline mock_cache = Mock() mock_get_cache.return_value = mock_cache mock_notif = Mock() mock_notif.clear_history.return_value = None mock_get_notif.return_value = mock_notif response = client.post("/api/cleanup", params={"max_age_hours": 48}) assert response.status_code == 200 data = response.json() assert data["message"] == "Cleanup completed successfully" assert data["max_age_hours"] == 48 assert "timestamp" in data # Verify cleanup methods were called mock_pipeline.cleanup_completed_jobs.assert_called_once_with(48) mock_notif.clear_history.assert_called_once() def test_health_check_healthy(self, client): """Test GET /api/health endpoint when healthy.""" with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline, \ patch.dict('os.environ', {'DEEPSEEK_API_KEY': 'test_key'}): mock_pipeline = Mock() mock_pipeline.get_active_jobs.return_value = ["job1"] mock_get_pipeline.return_value = mock_pipeline response = client.get("/api/health") assert response.status_code == 200 data = response.json() assert data["status"] == "healthy" assert data["active_jobs"] == 1 assert data["deepseek_api_available"] is True assert "timestamp" in data def test_health_check_degraded(self, client): """Test GET /api/health endpoint when degraded.""" with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline, \ patch.dict('os.environ', {}, clear=True): # No API key mock_pipeline = Mock() mock_pipeline.get_active_jobs.return_value = [] mock_get_pipeline.return_value = mock_pipeline response = client.get("/api/health") assert response.status_code == 200 data = response.json() assert data["status"] == "degraded" assert data["active_jobs"] == 0 assert data["deepseek_api_available"] is False assert data["warning"] == "DeepSeek API key not configured" class TestPipelineAPIErrorHandling: """Test error handling in Pipeline API.""" def test_process_video_service_error(self, client): """Test handling of service errors in process endpoint.""" request_data = { "video_url": "https://youtube.com/watch?v=test123" } with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.process_video = AsyncMock( side_effect=Exception("Service temporarily unavailable") ) mock_get_pipeline.return_value = mock_pipeline response = client.post("/api/process", json=request_data) assert response.status_code == 500 assert "Failed to start processing" in response.json()["detail"] assert "Service temporarily unavailable" in response.json()["detail"] def test_stats_endpoint_error(self, client): """Test error handling in stats endpoint.""" with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.get_active_jobs.side_effect = Exception("Database error") mock_get_pipeline.return_value = mock_pipeline response = client.get("/api/stats") assert response.status_code == 500 assert "Failed to retrieve statistics" in response.json()["detail"] def test_cleanup_endpoint_error(self, client): """Test error handling in cleanup endpoint.""" with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.cleanup_completed_jobs = AsyncMock( side_effect=Exception("Cleanup failed") ) mock_get_pipeline.return_value = mock_pipeline response = client.post("/api/cleanup") assert response.status_code == 500 assert "Cleanup failed" in response.json()["detail"] def test_health_check_error(self, client): """Test error handling in health check endpoint.""" with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.get_active_jobs.side_effect = Exception("Health check failed") mock_get_pipeline.return_value = mock_pipeline response = client.get("/api/health") assert response.status_code == 503 assert "Health check failed" in response.json()["detail"] class TestPipelineAPIValidation: """Test request validation in Pipeline API.""" def test_process_video_missing_url(self, client): """Test process endpoint with missing URL.""" request_data = { "summary_length": "standard" } response = client.post("/api/process", json=request_data) assert response.status_code == 422 # Validation error def test_process_video_invalid_summary_length(self, client): """Test process endpoint with invalid summary length.""" request_data = { "video_url": "https://youtube.com/watch?v=test123", "summary_length": "invalid_length" } response = client.post("/api/process", json=request_data) # Should still work as validation may be handled in service layer # or accept any string value assert response.status_code in [200, 422] def test_process_video_invalid_quality_threshold(self, client): """Test process endpoint with invalid quality threshold.""" request_data = { "video_url": "https://youtube.com/watch?v=test123", "quality_threshold": 1.5 # Should be 0.0-1.0 } with patch('backend.api.pipeline.get_summary_pipeline') as mock_get_pipeline: mock_pipeline = Mock() mock_pipeline.process_video = AsyncMock(return_value="test_job") mock_get_pipeline.return_value = mock_pipeline response = client.post("/api/process", json=request_data) # Should either validate or pass through assert response.status_code in [200, 422]