- Add SetupService for detecting application setup completion - Create ApplicationFlowMiddleware to enforce setup auth main flow - Add setup processing endpoints (/api/auth/setup, /api/auth/setup/status) - Add Pydantic models for setup requests and responses - Integrate middleware into FastAPI application - Fix logging paths to use ./logs consistently - All existing templates (setup.html, login.html) already working
440 lines
17 KiB
Python
440 lines
17 KiB
Python
"""
|
|
End-to-End tests for bulk download and export flows.
|
|
|
|
This module tests complete user workflows for bulk operations including
|
|
download flows, export processes, and error handling scenarios.
|
|
"""
|
|
|
|
import asyncio
|
|
import time
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from src.server.fastapi_app import app
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""Create a test client for the FastAPI application."""
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_headers(client):
|
|
"""Provide authentication headers for protected endpoints."""
|
|
# Login to get token
|
|
login_data = {"password": "testpassword"}
|
|
|
|
with patch('src.server.fastapi_app.settings.master_password_hash') as mock_hash:
|
|
mock_hash.return_value = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" # 'password' hash
|
|
response = client.post("/auth/login", json=login_data)
|
|
|
|
if response.status_code == 200:
|
|
token = response.json()["access_token"]
|
|
return {"Authorization": f"Bearer {token}"}
|
|
return {}
|
|
|
|
|
|
class TestBulkDownloadFlow:
|
|
"""End-to-end tests for bulk download workflows."""
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_complete_bulk_download_workflow(self, mock_user, client):
|
|
"""Test complete bulk download workflow from search to completion."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
# Step 1: Search for anime to download
|
|
search_response = client.get("/api/anime/search?q=test&limit=5")
|
|
if search_response.status_code == 200:
|
|
anime_list = search_response.json()
|
|
anime_ids = [anime["id"] for anime in anime_list[:3]] # Select first 3
|
|
else:
|
|
# Mock anime IDs if search endpoint not working
|
|
anime_ids = ["anime1", "anime2", "anime3"]
|
|
|
|
# Step 2: Initiate bulk download
|
|
download_request = {
|
|
"anime_ids": anime_ids,
|
|
"quality": "1080p",
|
|
"format": "mp4",
|
|
"include_subtitles": True,
|
|
"organize_by": "series"
|
|
}
|
|
|
|
download_response = client.post("/api/bulk/download", json=download_request)
|
|
# Expected 404 since bulk endpoints not implemented yet
|
|
assert download_response.status_code in [200, 202, 404]
|
|
|
|
if download_response.status_code in [200, 202]:
|
|
download_data = download_response.json()
|
|
task_id = download_data.get("task_id")
|
|
|
|
# Step 3: Monitor download progress
|
|
if task_id:
|
|
progress_response = client.get(f"/api/bulk/download/{task_id}/status")
|
|
assert progress_response.status_code in [200, 404]
|
|
|
|
if progress_response.status_code == 200:
|
|
progress_data = progress_response.json()
|
|
assert "status" in progress_data
|
|
assert "progress_percent" in progress_data
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_download_with_retry_logic(self, mock_user, client):
|
|
"""Test bulk download with retry logic for failed items."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
# Start bulk download
|
|
download_request = {
|
|
"anime_ids": ["anime1", "anime2", "anime3"],
|
|
"quality": "720p",
|
|
"retry_failed": True,
|
|
"max_retries": 3
|
|
}
|
|
|
|
download_response = client.post("/api/bulk/download", json=download_request)
|
|
assert download_response.status_code in [200, 202, 404]
|
|
|
|
if download_response.status_code in [200, 202]:
|
|
task_id = download_response.json().get("task_id")
|
|
|
|
# Simulate checking for failed items and retrying
|
|
if task_id:
|
|
failed_response = client.get(f"/api/bulk/download/{task_id}/failed")
|
|
assert failed_response.status_code in [200, 404]
|
|
|
|
if failed_response.status_code == 200:
|
|
failed_data = failed_response.json()
|
|
if failed_data.get("failed_items"):
|
|
# Retry failed items
|
|
retry_response = client.post(f"/api/bulk/download/{task_id}/retry")
|
|
assert retry_response.status_code in [200, 404]
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_download_cancellation(self, mock_user, client):
|
|
"""Test cancelling an ongoing bulk download."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
# Start bulk download
|
|
download_request = {
|
|
"anime_ids": ["anime1", "anime2", "anime3", "anime4", "anime5"],
|
|
"quality": "1080p"
|
|
}
|
|
|
|
download_response = client.post("/api/bulk/download", json=download_request)
|
|
assert download_response.status_code in [200, 202, 404]
|
|
|
|
if download_response.status_code in [200, 202]:
|
|
task_id = download_response.json().get("task_id")
|
|
|
|
if task_id:
|
|
# Cancel the download
|
|
cancel_response = client.post(f"/api/bulk/download/{task_id}/cancel")
|
|
assert cancel_response.status_code in [200, 404]
|
|
|
|
if cancel_response.status_code == 200:
|
|
cancel_data = cancel_response.json()
|
|
assert cancel_data.get("status") == "cancelled"
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_download_with_insufficient_space(self, mock_user, client):
|
|
"""Test bulk download when there's insufficient disk space."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
# Try to download large amount of content
|
|
download_request = {
|
|
"anime_ids": [f"anime{i}" for i in range(100)], # Large number
|
|
"quality": "1080p",
|
|
"check_disk_space": True
|
|
}
|
|
|
|
download_response = client.post("/api/bulk/download", json=download_request)
|
|
# Should either work or return appropriate error
|
|
assert download_response.status_code in [200, 202, 400, 404, 507] # 507 = Insufficient Storage
|
|
|
|
|
|
class TestBulkExportFlow:
|
|
"""End-to-end tests for bulk export workflows."""
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_complete_bulk_export_workflow(self, mock_user, client):
|
|
"""Test complete bulk export workflow."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
# Step 1: Get list of available anime for export
|
|
anime_response = client.get("/api/anime/search?limit=10")
|
|
if anime_response.status_code == 200:
|
|
anime_list = anime_response.json()
|
|
anime_ids = [anime["id"] for anime in anime_list[:5]]
|
|
else:
|
|
anime_ids = ["anime1", "anime2", "anime3"]
|
|
|
|
# Step 2: Request bulk export
|
|
export_request = {
|
|
"anime_ids": anime_ids,
|
|
"format": "json",
|
|
"include_metadata": True,
|
|
"include_episode_info": True,
|
|
"include_download_history": False
|
|
}
|
|
|
|
export_response = client.post("/api/bulk/export", json=export_request)
|
|
assert export_response.status_code in [200, 202, 404]
|
|
|
|
if export_response.status_code in [200, 202]:
|
|
export_data = export_response.json()
|
|
|
|
# Step 3: Check export status or get download URL
|
|
if "export_id" in export_data:
|
|
export_id = export_data["export_id"]
|
|
status_response = client.get(f"/api/bulk/export/{export_id}/status")
|
|
assert status_response.status_code in [200, 404]
|
|
elif "download_url" in export_data:
|
|
# Direct download available
|
|
download_url = export_data["download_url"]
|
|
assert download_url.startswith("http")
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_export_csv_format(self, mock_user, client):
|
|
"""Test bulk export in CSV format."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
export_request = {
|
|
"anime_ids": ["anime1", "anime2"],
|
|
"format": "csv",
|
|
"include_metadata": True,
|
|
"csv_options": {
|
|
"delimiter": ",",
|
|
"include_headers": True,
|
|
"encoding": "utf-8"
|
|
}
|
|
}
|
|
|
|
export_response = client.post("/api/bulk/export", json=export_request)
|
|
assert export_response.status_code in [200, 202, 404]
|
|
|
|
if export_response.status_code == 200:
|
|
# Check if response is CSV content or redirect
|
|
content_type = export_response.headers.get("content-type", "")
|
|
assert "csv" in content_type or "json" in content_type
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_export_with_filters(self, mock_user, client):
|
|
"""Test bulk export with filtering options."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
export_request = {
|
|
"anime_ids": ["anime1", "anime2", "anime3"],
|
|
"format": "json",
|
|
"filters": {
|
|
"completed_only": True,
|
|
"include_watched": False,
|
|
"min_rating": 7.0,
|
|
"genres": ["Action", "Adventure"]
|
|
},
|
|
"include_metadata": True
|
|
}
|
|
|
|
export_response = client.post("/api/bulk/export", json=export_request)
|
|
assert export_response.status_code in [200, 202, 404]
|
|
|
|
|
|
class TestBulkOrganizeFlow:
|
|
"""End-to-end tests for bulk organize workflows."""
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_organize_by_genre(self, mock_user, client):
|
|
"""Test bulk organizing anime by genre."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
organize_request = {
|
|
"anime_ids": ["anime1", "anime2", "anime3"],
|
|
"organize_by": "genre",
|
|
"create_subdirectories": True,
|
|
"move_files": True,
|
|
"update_database": True
|
|
}
|
|
|
|
organize_response = client.post("/api/bulk/organize", json=organize_request)
|
|
assert organize_response.status_code in [200, 202, 404]
|
|
|
|
if organize_response.status_code in [200, 202]:
|
|
organize_data = organize_response.json()
|
|
|
|
if "task_id" in organize_data:
|
|
task_id = organize_data["task_id"]
|
|
# Monitor organization progress
|
|
status_response = client.get(f"/api/bulk/organize/{task_id}/status")
|
|
assert status_response.status_code in [200, 404]
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_organize_by_year(self, mock_user, client):
|
|
"""Test bulk organizing anime by release year."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
organize_request = {
|
|
"anime_ids": ["anime1", "anime2"],
|
|
"organize_by": "year",
|
|
"year_format": "YYYY",
|
|
"create_subdirectories": True,
|
|
"dry_run": True # Test without actually moving files
|
|
}
|
|
|
|
organize_response = client.post("/api/bulk/organize", json=organize_request)
|
|
assert organize_response.status_code in [200, 404]
|
|
|
|
if organize_response.status_code == 200:
|
|
organize_data = organize_response.json()
|
|
# Dry run should return what would be moved
|
|
assert "preview" in organize_data or "operations" in organize_data
|
|
|
|
|
|
class TestBulkDeleteFlow:
|
|
"""End-to-end tests for bulk delete workflows."""
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_delete_with_confirmation(self, mock_user, client):
|
|
"""Test bulk delete with proper confirmation flow."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
# Step 1: Request deletion (should require confirmation)
|
|
delete_request = {
|
|
"anime_ids": ["anime_to_delete1", "anime_to_delete2"],
|
|
"delete_files": True,
|
|
"confirm": False # First request without confirmation
|
|
}
|
|
|
|
delete_response = client.delete("/api/bulk/delete", json=delete_request)
|
|
# Should require confirmation
|
|
assert delete_response.status_code in [400, 404, 422]
|
|
|
|
# Step 2: Confirm deletion
|
|
delete_request["confirm"] = True
|
|
confirmed_response = client.delete("/api/bulk/delete", json=delete_request)
|
|
assert confirmed_response.status_code in [200, 404]
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_delete_database_only(self, mock_user, client):
|
|
"""Test bulk delete from database only (keep files)."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
delete_request = {
|
|
"anime_ids": ["anime1", "anime2"],
|
|
"delete_files": False, # Keep files, remove from database only
|
|
"confirm": True
|
|
}
|
|
|
|
delete_response = client.delete("/api/bulk/delete", json=delete_request)
|
|
assert delete_response.status_code in [200, 404]
|
|
|
|
|
|
class TestBulkOperationsErrorHandling:
|
|
"""End-to-end tests for error handling in bulk operations."""
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_operation_with_mixed_results(self, mock_user, client):
|
|
"""Test bulk operation where some items succeed and others fail."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
# Mix of valid and invalid anime IDs
|
|
download_request = {
|
|
"anime_ids": ["valid_anime1", "invalid_anime", "valid_anime2"],
|
|
"quality": "1080p",
|
|
"continue_on_error": True
|
|
}
|
|
|
|
download_response = client.post("/api/bulk/download", json=download_request)
|
|
assert download_response.status_code in [200, 202, 404]
|
|
|
|
if download_response.status_code in [200, 202]:
|
|
result_data = download_response.json()
|
|
# Should have information about successes and failures
|
|
if "partial_success" in result_data:
|
|
assert "successful" in result_data
|
|
assert "failed" in result_data
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_operation_timeout_handling(self, mock_user, client):
|
|
"""Test handling of bulk operation timeouts."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
# Large operation that might timeout
|
|
large_request = {
|
|
"anime_ids": [f"anime{i}" for i in range(50)],
|
|
"quality": "1080p",
|
|
"timeout_seconds": 30
|
|
}
|
|
|
|
download_response = client.post("/api/bulk/download", json=large_request)
|
|
# Should either succeed, be accepted for background processing, or timeout
|
|
assert download_response.status_code in [200, 202, 404, 408, 504]
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_concurrent_bulk_operations(self, mock_user, client):
|
|
"""Test handling of concurrent bulk operations."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
# Start first operation
|
|
first_request = {
|
|
"anime_ids": ["anime1", "anime2"],
|
|
"quality": "1080p"
|
|
}
|
|
|
|
first_response = client.post("/api/bulk/download", json=first_request)
|
|
|
|
# Start second operation while first is running
|
|
second_request = {
|
|
"anime_ids": ["anime3", "anime4"],
|
|
"quality": "720p"
|
|
}
|
|
|
|
second_response = client.post("/api/bulk/download", json=second_request)
|
|
|
|
# Both operations should be handled appropriately
|
|
assert first_response.status_code in [200, 202, 404]
|
|
assert second_response.status_code in [200, 202, 404, 429] # 429 = Too Many Requests
|
|
|
|
|
|
class TestBulkOperationsPerformance:
|
|
"""Performance tests for bulk operations."""
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_operation_response_time(self, mock_user, client):
|
|
"""Test that bulk operations respond within reasonable time."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
start_time = time.time()
|
|
|
|
download_request = {
|
|
"anime_ids": ["anime1", "anime2", "anime3"],
|
|
"quality": "1080p"
|
|
}
|
|
|
|
response = client.post("/api/bulk/download", json=download_request)
|
|
|
|
response_time = time.time() - start_time
|
|
|
|
# Response should be quick (< 5 seconds) even if processing is background
|
|
assert response_time < 5.0
|
|
assert response.status_code in [200, 202, 404]
|
|
|
|
@patch('src.server.fastapi_app.get_current_user')
|
|
def test_bulk_operation_memory_usage(self, mock_user, client):
|
|
"""Test bulk operations don't cause excessive memory usage."""
|
|
mock_user.return_value = {"user_id": "test_user"}
|
|
|
|
# Large bulk operation
|
|
large_request = {
|
|
"anime_ids": [f"anime{i}" for i in range(100)],
|
|
"quality": "1080p"
|
|
}
|
|
|
|
# This test would need actual memory monitoring in real implementation
|
|
response = client.post("/api/bulk/download", json=large_request)
|
|
assert response.status_code in [200, 202, 404, 413] # 413 = Payload Too Large
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"]) |