Aniworld/tests/unit/web/controllers/api/v1/test_downloads.py

717 lines
25 KiB
Python

"""
Test cases for downloads API endpoints.
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from flask import Flask
import json
# Mock the database managers first
mock_download_manager = Mock()
mock_episode_manager = Mock()
mock_anime_manager = Mock()
# Import the modules to test
try:
with patch.dict('sys.modules', {
'src.server.data.download_manager': Mock(DownloadManager=Mock(return_value=mock_download_manager)),
'src.server.data.episode_manager': Mock(EpisodeManager=Mock(return_value=mock_episode_manager)),
'src.server.data.anime_manager': Mock(AnimeManager=Mock(return_value=mock_anime_manager))
}):
from src.server.web.controllers.api.v1.downloads import downloads_bp
except ImportError:
downloads_bp = None
class TestDownloadEndpoints:
"""Test cases for download API endpoints."""
@pytest.fixture
def app(self):
"""Create a test Flask application."""
if not downloads_bp:
pytest.skip("Module not available")
app = Flask(__name__)
app.config['TESTING'] = True
app.register_blueprint(downloads_bp, url_prefix='/api/v1')
return app
@pytest.fixture
def client(self, app):
"""Create a test client."""
return app.test_client()
@pytest.fixture
def mock_session(self):
"""Mock session for authentication."""
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
mock_session.get.return_value = {'user_id': 1, 'username': 'testuser'}
yield mock_session
def setup_method(self):
"""Reset mocks before each test."""
mock_download_manager.reset_mock()
mock_episode_manager.reset_mock()
mock_anime_manager.reset_mock()
def test_list_downloads_success(self, client, mock_session):
"""Test GET /downloads - list downloads with pagination."""
if not downloads_bp:
pytest.skip("Module not available")
mock_downloads = [
{
'id': 1,
'anime_id': 1,
'episode_id': 1,
'status': 'downloading',
'progress': 45.5,
'size': 1073741824, # 1GB
'downloaded_size': 488447385, # ~465MB
'speed': 1048576, # 1MB/s
'eta': 600, # 10 minutes
'created_at': '2023-01-01 12:00:00'
},
{
'id': 2,
'anime_id': 1,
'episode_id': 2,
'status': 'completed',
'progress': 100.0,
'size': 1073741824,
'downloaded_size': 1073741824,
'created_at': '2023-01-01 11:00:00',
'completed_at': '2023-01-01 11:30:00'
}
]
mock_download_manager.get_all_downloads.return_value = mock_downloads
mock_download_manager.get_downloads_count.return_value = 2
response = client.get('/api/v1/downloads?page=1&per_page=10')
assert response.status_code == 200
data = json.loads(response.data)
assert 'data' in data
assert 'pagination' in data
assert len(data['data']) == 2
assert data['data'][0]['status'] == 'downloading'
mock_download_manager.get_all_downloads.assert_called_once_with(
offset=0, limit=10, status=None, anime_id=None, sort_by='created_at', sort_order='desc'
)
def test_list_downloads_with_filters(self, client, mock_session):
"""Test GET /downloads with filters."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.get_all_downloads.return_value = []
mock_download_manager.get_downloads_count.return_value = 0
response = client.get('/api/v1/downloads?status=completed&anime_id=5&sort_by=progress')
assert response.status_code == 200
mock_download_manager.get_all_downloads.assert_called_once_with(
offset=0, limit=20, status='completed', anime_id=5, sort_by='progress', sort_order='desc'
)
def test_get_download_by_id_success(self, client, mock_session):
"""Test GET /downloads/<id> - get specific download."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download = {
'id': 1,
'anime_id': 1,
'episode_id': 1,
'status': 'downloading',
'progress': 75.0,
'size': 1073741824,
'downloaded_size': 805306368,
'speed': 2097152, # 2MB/s
'eta': 150, # 2.5 minutes
'file_path': '/downloads/anime1/episode1.mp4',
'created_at': '2023-01-01 12:00:00',
'started_at': '2023-01-01 12:05:00'
}
mock_download_manager.get_download_by_id.return_value = mock_download
response = client.get('/api/v1/downloads/1')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert data['data']['id'] == 1
assert data['data']['progress'] == 75.0
assert data['data']['status'] == 'downloading'
mock_download_manager.get_download_by_id.assert_called_once_with(1)
def test_get_download_by_id_not_found(self, client, mock_session):
"""Test GET /downloads/<id> - download not found."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.get_download_by_id.return_value = None
response = client.get('/api/v1/downloads/999')
assert response.status_code == 404
data = json.loads(response.data)
assert 'error' in data
assert 'not found' in data['error'].lower()
def test_create_download_success(self, client, mock_session):
"""Test POST /downloads - create new download."""
if not downloads_bp:
pytest.skip("Module not available")
download_data = {
'episode_id': 1,
'quality': '1080p',
'priority': 'normal'
}
# Mock episode exists
mock_episode_manager.get_episode_by_id.return_value = {
'id': 1,
'anime_id': 1,
'title': 'Episode 1',
'url': 'https://example.com/episode/1'
}
mock_download_manager.create_download.return_value = 1
mock_download_manager.get_download_by_id.return_value = {
'id': 1,
'episode_id': 1,
'status': 'queued',
'progress': 0.0
}
response = client.post('/api/v1/downloads',
json=download_data,
content_type='application/json')
assert response.status_code == 201
data = json.loads(response.data)
assert data['success'] is True
assert data['data']['id'] == 1
assert data['data']['status'] == 'queued'
mock_download_manager.create_download.assert_called_once()
def test_create_download_invalid_episode(self, client, mock_session):
"""Test POST /downloads - invalid episode_id."""
if not downloads_bp:
pytest.skip("Module not available")
download_data = {
'episode_id': 999,
'quality': '1080p'
}
# Mock episode doesn't exist
mock_episode_manager.get_episode_by_id.return_value = None
response = client.post('/api/v1/downloads',
json=download_data,
content_type='application/json')
assert response.status_code == 400
data = json.loads(response.data)
assert 'error' in data
assert 'episode' in data['error'].lower()
def test_create_download_validation_error(self, client, mock_session):
"""Test POST /downloads - validation error."""
if not downloads_bp:
pytest.skip("Module not available")
# Missing required fields
download_data = {
'quality': '1080p'
}
response = client.post('/api/v1/downloads',
json=download_data,
content_type='application/json')
assert response.status_code == 400
data = json.loads(response.data)
assert 'error' in data
def test_pause_download_success(self, client, mock_session):
"""Test PUT /downloads/<id>/pause - pause download."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.get_download_by_id.return_value = {
'id': 1,
'status': 'downloading'
}
mock_download_manager.pause_download.return_value = True
response = client.put('/api/v1/downloads/1/pause')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert 'paused' in data['message'].lower()
mock_download_manager.pause_download.assert_called_once_with(1)
def test_pause_download_not_found(self, client, mock_session):
"""Test PUT /downloads/<id>/pause - download not found."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.get_download_by_id.return_value = None
response = client.put('/api/v1/downloads/999/pause')
assert response.status_code == 404
data = json.loads(response.data)
assert 'error' in data
def test_resume_download_success(self, client, mock_session):
"""Test PUT /downloads/<id>/resume - resume download."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.get_download_by_id.return_value = {
'id': 1,
'status': 'paused'
}
mock_download_manager.resume_download.return_value = True
response = client.put('/api/v1/downloads/1/resume')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert 'resumed' in data['message'].lower()
mock_download_manager.resume_download.assert_called_once_with(1)
def test_cancel_download_success(self, client, mock_session):
"""Test DELETE /downloads/<id> - cancel download."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.get_download_by_id.return_value = {
'id': 1,
'status': 'downloading'
}
mock_download_manager.cancel_download.return_value = True
response = client.delete('/api/v1/downloads/1')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert 'cancelled' in data['message'].lower()
mock_download_manager.cancel_download.assert_called_once_with(1)
def test_get_download_queue_success(self, client, mock_session):
"""Test GET /downloads/queue - get download queue."""
if not downloads_bp:
pytest.skip("Module not available")
mock_queue = [
{
'id': 1,
'episode_id': 1,
'status': 'downloading',
'progress': 25.0,
'position': 1
},
{
'id': 2,
'episode_id': 2,
'status': 'queued',
'progress': 0.0,
'position': 2
}
]
mock_download_manager.get_download_queue.return_value = mock_queue
response = client.get('/api/v1/downloads/queue')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert len(data['data']) == 2
assert data['data'][0]['status'] == 'downloading'
assert data['data'][1]['status'] == 'queued'
def test_reorder_download_queue_success(self, client, mock_session):
"""Test PUT /downloads/queue/reorder - reorder download queue."""
if not downloads_bp:
pytest.skip("Module not available")
reorder_data = {
'download_ids': [3, 1, 2] # New order
}
mock_download_manager.reorder_download_queue.return_value = True
response = client.put('/api/v1/downloads/queue/reorder',
json=reorder_data,
content_type='application/json')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
mock_download_manager.reorder_download_queue.assert_called_once_with([3, 1, 2])
def test_clear_download_queue_success(self, client, mock_session):
"""Test DELETE /downloads/queue - clear download queue."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.clear_download_queue.return_value = {
'cleared': 5,
'failed': 0
}
response = client.delete('/api/v1/downloads/queue')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert data['data']['cleared'] == 5
mock_download_manager.clear_download_queue.assert_called_once()
def test_get_download_history_success(self, client, mock_session):
"""Test GET /downloads/history - get download history."""
if not downloads_bp:
pytest.skip("Module not available")
mock_history = [
{
'id': 1,
'episode_id': 1,
'status': 'completed',
'completed_at': '2023-01-01 12:30:00',
'file_size': 1073741824
},
{
'id': 2,
'episode_id': 2,
'status': 'failed',
'failed_at': '2023-01-01 11:45:00',
'error_message': 'Network timeout'
}
]
mock_download_manager.get_download_history.return_value = mock_history
mock_download_manager.get_history_count.return_value = 2
response = client.get('/api/v1/downloads/history?page=1&per_page=10')
assert response.status_code == 200
data = json.loads(response.data)
assert 'data' in data
assert 'pagination' in data
assert len(data['data']) == 2
assert data['data'][0]['status'] == 'completed'
def test_bulk_create_downloads_success(self, client, mock_session):
"""Test POST /downloads/bulk - bulk create downloads."""
if not downloads_bp:
pytest.skip("Module not available")
bulk_data = {
'downloads': [
{'episode_id': 1, 'quality': '1080p'},
{'episode_id': 2, 'quality': '720p'},
{'episode_id': 3, 'quality': '1080p'}
]
}
mock_download_manager.bulk_create_downloads.return_value = {
'created': 3,
'failed': 0,
'created_ids': [1, 2, 3]
}
response = client.post('/api/v1/downloads/bulk',
json=bulk_data,
content_type='application/json')
assert response.status_code == 201
data = json.loads(response.data)
assert data['success'] is True
assert data['data']['created'] == 3
assert data['data']['failed'] == 0
def test_bulk_pause_downloads_success(self, client, mock_session):
"""Test PUT /downloads/bulk/pause - bulk pause downloads."""
if not downloads_bp:
pytest.skip("Module not available")
bulk_data = {
'download_ids': [1, 2, 3]
}
mock_download_manager.bulk_pause_downloads.return_value = {
'paused': 3,
'failed': 0
}
response = client.put('/api/v1/downloads/bulk/pause',
json=bulk_data,
content_type='application/json')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert data['data']['paused'] == 3
def test_bulk_resume_downloads_success(self, client, mock_session):
"""Test PUT /downloads/bulk/resume - bulk resume downloads."""
if not downloads_bp:
pytest.skip("Module not available")
bulk_data = {
'download_ids': [1, 2, 3]
}
mock_download_manager.bulk_resume_downloads.return_value = {
'resumed': 3,
'failed': 0
}
response = client.put('/api/v1/downloads/bulk/resume',
json=bulk_data,
content_type='application/json')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert data['data']['resumed'] == 3
def test_bulk_cancel_downloads_success(self, client, mock_session):
"""Test DELETE /downloads/bulk - bulk cancel downloads."""
if not downloads_bp:
pytest.skip("Module not available")
bulk_data = {
'download_ids': [1, 2, 3]
}
mock_download_manager.bulk_cancel_downloads.return_value = {
'cancelled': 3,
'failed': 0
}
response = client.delete('/api/v1/downloads/bulk',
json=bulk_data,
content_type='application/json')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert data['data']['cancelled'] == 3
def test_get_download_stats_success(self, client, mock_session):
"""Test GET /downloads/stats - get download statistics."""
if not downloads_bp:
pytest.skip("Module not available")
mock_stats = {
'total_downloads': 150,
'completed_downloads': 125,
'active_downloads': 3,
'failed_downloads': 22,
'total_size_downloaded': 107374182400, # 100GB
'average_speed': 2097152, # 2MB/s
'queue_size': 5
}
mock_download_manager.get_download_stats.return_value = mock_stats
response = client.get('/api/v1/downloads/stats')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert data['data']['total_downloads'] == 150
assert data['data']['completed_downloads'] == 125
assert data['data']['active_downloads'] == 3
def test_retry_failed_download_success(self, client, mock_session):
"""Test PUT /downloads/<id>/retry - retry failed download."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.get_download_by_id.return_value = {
'id': 1,
'status': 'failed'
}
mock_download_manager.retry_download.return_value = True
response = client.put('/api/v1/downloads/1/retry')
assert response.status_code == 200
data = json.loads(response.data)
assert data['success'] is True
assert 'retrying' in data['message'].lower()
mock_download_manager.retry_download.assert_called_once_with(1)
def test_retry_download_invalid_status(self, client, mock_session):
"""Test PUT /downloads/<id>/retry - retry download with invalid status."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.get_download_by_id.return_value = {
'id': 1,
'status': 'completed' # Can't retry completed downloads
}
response = client.put('/api/v1/downloads/1/retry')
assert response.status_code == 400
data = json.loads(response.data)
assert 'error' in data
assert 'cannot be retried' in data['error'].lower()
class TestDownloadAuthentication:
"""Test cases for download endpoints authentication."""
@pytest.fixture
def app(self):
"""Create a test Flask application."""
if not downloads_bp:
pytest.skip("Module not available")
app = Flask(__name__)
app.config['TESTING'] = True
app.register_blueprint(downloads_bp, url_prefix='/api/v1')
return app
@pytest.fixture
def client(self, app):
"""Create a test client."""
return app.test_client()
def test_unauthenticated_read_access(self, client):
"""Test that read operations work without authentication."""
if not downloads_bp:
pytest.skip("Module not available")
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
mock_session.get.return_value = None # No authentication
mock_download_manager.get_all_downloads.return_value = []
mock_download_manager.get_downloads_count.return_value = 0
response = client.get('/api/v1/downloads')
# Should work for read operations
assert response.status_code == 200
def test_authenticated_write_access(self, client):
"""Test that write operations require authentication."""
if not downloads_bp:
pytest.skip("Module not available")
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
mock_session.get.return_value = None # No authentication
response = client.post('/api/v1/downloads',
json={'episode_id': 1},
content_type='application/json')
# Should require authentication for write operations
assert response.status_code == 401
class TestDownloadErrorHandling:
"""Test cases for download endpoints error handling."""
@pytest.fixture
def app(self):
"""Create a test Flask application."""
if not downloads_bp:
pytest.skip("Module not available")
app = Flask(__name__)
app.config['TESTING'] = True
app.register_blueprint(downloads_bp, url_prefix='/api/v1')
return app
@pytest.fixture
def client(self, app):
"""Create a test client."""
return app.test_client()
@pytest.fixture
def mock_session(self):
"""Mock session for authentication."""
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
mock_session.get.return_value = {'user_id': 1, 'username': 'testuser'}
yield mock_session
def test_database_error_handling(self, client, mock_session):
"""Test handling of database errors."""
if not downloads_bp:
pytest.skip("Module not available")
# Simulate database error
mock_download_manager.get_all_downloads.side_effect = Exception("Database connection failed")
response = client.get('/api/v1/downloads')
assert response.status_code == 500
data = json.loads(response.data)
assert 'error' in data
def test_download_system_error(self, client, mock_session):
"""Test handling of download system errors."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.get_download_by_id.return_value = {
'id': 1,
'status': 'downloading'
}
# Simulate download system error
mock_download_manager.pause_download.side_effect = Exception("Download system unavailable")
response = client.put('/api/v1/downloads/1/pause')
assert response.status_code == 500
data = json.loads(response.data)
assert 'error' in data
def test_invalid_download_status_transition(self, client, mock_session):
"""Test handling of invalid status transitions."""
if not downloads_bp:
pytest.skip("Module not available")
mock_download_manager.get_download_by_id.return_value = {
'id': 1,
'status': 'completed'
}
# Try to pause a completed download
response = client.put('/api/v1/downloads/1/pause')
assert response.status_code == 400
data = json.loads(response.data)
assert 'error' in data
assert 'cannot be paused' in data['error'].lower()
if __name__ == '__main__':
pytest.main([__file__])