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