youtube-summarizer/backend/tests/integration/test_export_api.py

464 lines
16 KiB
Python

"""
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