""" Integration tests for Export API endpoints Tests all export endpoints with FastAPI TestClient """ import pytest import json import os import tempfile from pathlib import Path from fastapi.testclient import TestClient from unittest.mock import Mock, patch, AsyncMock from datetime import datetime # Add parent directory to path import sys sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) from backend.main import app from backend.services.export_service import ( ExportService, ExportFormat, ExportStatus, ExportResult ) # Create test client client = TestClient(app) @pytest.fixture def mock_export_service(): """Mock export service for testing""" with patch('backend.api.export.export_service') as mock_service: # Configure mock mock_service.export_summary = AsyncMock() mock_service.bulk_export_summaries = AsyncMock() mock_service.get_export_status = Mock() mock_service.cleanup_old_exports = AsyncMock() mock_service.active_exports = {} mock_service.exporters = { ExportFormat.MARKDOWN: Mock(), ExportFormat.JSON: Mock(), ExportFormat.PLAIN_TEXT: Mock(), ExportFormat.HTML: Mock() } yield mock_service @pytest.fixture def mock_summary_data(): """Mock summary data for testing""" with patch('backend.api.export.get_summary_data') as mock_get: mock_get.return_value = { "video_id": "test123", "video_url": "https://youtube.com/watch?v=test123", "video_metadata": { "title": "Test Video", "channel_name": "Test Channel" }, "summary": "Test summary", "key_points": ["Point 1", "Point 2"], "main_themes": ["Theme 1"], "created_at": datetime.utcnow().isoformat() } yield mock_get class TestExportAPI: """Test export API endpoints""" def test_single_export_success(self, mock_export_service, mock_summary_data): """Test successful single export""" # Configure mock response export_result = ExportResult( export_id="test-export-123", status=ExportStatus.COMPLETED, format=ExportFormat.MARKDOWN, file_path="/tmp/test.md", file_size_bytes=1024, download_url="/api/export/download/test-export-123", created_at=datetime.utcnow(), completed_at=datetime.utcnow() ) mock_export_service.export_summary.return_value = export_result response = client.post( "/api/export/single", json={ "summary_id": "test123", "format": "markdown", "include_metadata": True } ) assert response.status_code == 200 data = response.json() assert data["export_id"] == "test-export-123" assert data["status"] == "completed" assert data["format"] == "markdown" assert data["download_url"] == "/api/export/download/test-export-123" assert data["file_size_bytes"] == 1024 def test_single_export_summary_not_found(self, mock_export_service): """Test single export with non-existent summary""" with patch('backend.api.export.get_summary_data') as mock_get: mock_get.return_value = None response = client.post( "/api/export/single", json={ "summary_id": "nonexistent", "format": "markdown" } ) assert response.status_code == 404 assert "Summary not found" in response.json()["detail"] def test_single_export_invalid_format(self): """Test single export with invalid format""" response = client.post( "/api/export/single", json={ "summary_id": "test123", "format": "invalid_format" } ) assert response.status_code == 422 # Validation error def test_single_export_with_branding(self, mock_export_service, mock_summary_data): """Test single export with custom branding""" export_result = ExportResult( export_id="test-export-123", status=ExportStatus.COMPLETED, format=ExportFormat.MARKDOWN, created_at=datetime.utcnow() ) mock_export_service.export_summary.return_value = export_result response = client.post( "/api/export/single", json={ "summary_id": "test123", "format": "markdown", "custom_branding": { "company_name": "Test Company", "primary_color": "#ff0000" } } ) assert response.status_code == 200 # Check that branding was passed to service call_args = mock_export_service.export_summary.call_args assert call_args[0][1].custom_branding["company_name"] == "Test Company" def test_bulk_export_success(self, mock_export_service, mock_summary_data): """Test successful bulk export""" export_result = ExportResult( export_id="bulk-export-123", status=ExportStatus.COMPLETED, format=ExportFormat.JSON, file_path="/tmp/bulk.zip", file_size_bytes=5120, download_url="/api/export/download/bulk-export-123", created_at=datetime.utcnow(), completed_at=datetime.utcnow() ) mock_export_service.bulk_export_summaries.return_value = export_result response = client.post( "/api/export/bulk", json={ "summary_ids": ["test123", "test456"], "formats": ["markdown", "json"], "organize_by": "format" } ) assert response.status_code == 200 data = response.json() assert data["export_id"] == "bulk-export-123" assert data["status"] == "completed" def test_bulk_export_too_many_summaries(self): """Test bulk export with too many summaries""" summary_ids = [f"test{i}" for i in range(101)] # 101 summaries response = client.post( "/api/export/bulk", json={ "summary_ids": summary_ids, "formats": ["markdown"] } ) assert response.status_code == 400 assert "Maximum 100 summaries" in response.json()["detail"] def test_bulk_export_no_valid_summaries(self): """Test bulk export with no valid summaries""" with patch('backend.api.export.get_summary_data') as mock_get: mock_get.return_value = None response = client.post( "/api/export/bulk", json={ "summary_ids": ["invalid1", "invalid2"], "formats": ["markdown"] } ) assert response.status_code == 404 assert "No valid summaries found" in response.json()["detail"] def test_bulk_export_async_processing(self, mock_export_service): """Test bulk export with async processing for large batches""" # Mock large batch of summaries summary_ids = [f"test{i}" for i in range(15)] # > 10 triggers async with patch('backend.api.export.get_summary_data') as mock_get: mock_get.return_value = {"video_id": "test", "summary": "Test"} response = client.post( "/api/export/bulk", json={ "summary_ids": summary_ids, "formats": ["markdown"] } ) assert response.status_code == 200 data = response.json() assert data["status"] == "processing" assert data["estimated_time_remaining"] is not None def test_get_export_status_success(self, mock_export_service): """Test getting export status""" export_result = ExportResult( export_id="test-export-123", status=ExportStatus.PROCESSING, format=ExportFormat.MARKDOWN, created_at=datetime.utcnow() ) mock_export_service.get_export_status.return_value = export_result response = client.get("/api/export/status/test-export-123") assert response.status_code == 200 data = response.json() assert data["export_id"] == "test-export-123" assert data["status"] == "processing" def test_get_export_status_not_found(self, mock_export_service): """Test getting status for non-existent export""" mock_export_service.get_export_status.return_value = None response = client.get("/api/export/status/nonexistent") assert response.status_code == 404 assert "Export not found" in response.json()["detail"] def test_download_export_success(self, mock_export_service): """Test downloading export file""" # Create a temporary file with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: f.write("# Test Export") temp_file = f.name try: export_result = ExportResult( export_id="test-export-123", status=ExportStatus.COMPLETED, format=ExportFormat.MARKDOWN, file_path=temp_file ) mock_export_service.get_export_status.return_value = export_result response = client.get("/api/export/download/test-export-123") assert response.status_code == 200 assert response.headers["content-type"] == "text/markdown; charset=utf-8" assert "attachment" in response.headers["content-disposition"] assert b"# Test Export" in response.content finally: os.remove(temp_file) def test_download_export_not_found(self, mock_export_service): """Test downloading non-existent export""" mock_export_service.get_export_status.return_value = None response = client.get("/api/export/download/nonexistent") assert response.status_code == 404 assert "Export file not found" in response.json()["detail"] def test_download_export_file_missing(self, mock_export_service): """Test downloading export with missing file""" export_result = ExportResult( export_id="test-export-123", status=ExportStatus.COMPLETED, format=ExportFormat.MARKDOWN, file_path="/nonexistent/file.md" ) mock_export_service.get_export_status.return_value = export_result response = client.get("/api/export/download/test-export-123") assert response.status_code == 404 assert "no longer available" in response.json()["detail"] def test_list_exports(self, mock_export_service): """Test listing exports with pagination""" # Create mock exports exports = [] for i in range(5): exports.append(ExportResult( export_id=f"export-{i}", status=ExportStatus.COMPLETED, format=ExportFormat.MARKDOWN, created_at=datetime.utcnow() )) mock_export_service.active_exports = { e.export_id: e for e in exports } response = client.get("/api/export/list?page=1&page_size=2") assert response.status_code == 200 data = response.json() assert data["total"] == 5 assert len(data["exports"]) == 2 assert data["page"] == 1 assert data["page_size"] == 2 def test_list_exports_with_status_filter(self, mock_export_service): """Test listing exports filtered by status""" # Create exports with different statuses completed = ExportResult( export_id="completed-1", status=ExportStatus.COMPLETED, format=ExportFormat.MARKDOWN, created_at=datetime.utcnow() ) processing = ExportResult( export_id="processing-1", status=ExportStatus.PROCESSING, format=ExportFormat.JSON, created_at=datetime.utcnow() ) mock_export_service.active_exports = { "completed-1": completed, "processing-1": processing } response = client.get("/api/export/list?status=completed") assert response.status_code == 200 data = response.json() assert data["total"] == 1 assert data["exports"][0]["status"] == "completed" def test_cleanup_exports(self, mock_export_service): """Test cleanup of old exports""" mock_export_service.cleanup_old_exports.return_value = None response = client.delete("/api/export/cleanup?max_age_hours=12") assert response.status_code == 200 assert "Cleaned up exports" in response.json()["message"] # Check cleanup was called with correct parameter mock_export_service.cleanup_old_exports.assert_called_once_with(12) def test_get_available_formats(self, mock_export_service): """Test getting available export formats""" response = client.get("/api/export/formats") assert response.status_code == 200 data = response.json() assert "formats" in data # Check that all formats are listed format_names = [f["format"] for f in data["formats"]] assert "markdown" in format_names assert "json" in format_names assert "text" in format_names assert "html" in format_names assert "pdf" in format_names # Check format details markdown_format = next(f for f in data["formats"] if f["format"] == "markdown") assert markdown_format["available"] == True assert "documentation" in markdown_format["description"].lower() class TestExportValidation: """Test export request validation""" def test_single_export_missing_summary_id(self): """Test single export without summary_id""" response = client.post( "/api/export/single", json={ "format": "markdown" } ) assert response.status_code == 422 def test_single_export_missing_format(self): """Test single export without format""" response = client.post( "/api/export/single", json={ "summary_id": "test123" } ) assert response.status_code == 422 def test_bulk_export_missing_summary_ids(self): """Test bulk export without summary_ids""" response = client.post( "/api/export/bulk", json={ "formats": ["markdown"] } ) assert response.status_code == 422 def test_bulk_export_empty_summary_ids(self): """Test bulk export with empty summary_ids""" response = client.post( "/api/export/bulk", json={ "summary_ids": [], "formats": ["markdown"] } ) assert response.status_code == 422 def test_bulk_export_invalid_organize_by(self): """Test bulk export with invalid organize_by value""" response = client.post( "/api/export/bulk", json={ "summary_ids": ["test123"], "formats": ["markdown"], "organize_by": "invalid" } ) # Should still work as it defaults to "format" if invalid assert response.status_code in [200, 404] # Depends on summary existence