Aniworld/src/tests/e2e/test_bulk_operations_flow.py
Lukas Pupka-Lipinski 3f98dd6ebb Implement application setup and flow middleware
- 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
2025-10-06 12:48:18 +02:00

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