464 lines
16 KiB
Python
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 |