""" Test cases for episodes API endpoints. """ import pytest from unittest.mock import Mock, patch, MagicMock from flask import Flask import json # Mock the database managers first mock_episode_manager = Mock() mock_anime_manager = Mock() mock_download_manager = Mock() # Import the modules to test try: with patch.dict('sys.modules', { '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)), 'src.server.data.download_manager': Mock(DownloadManager=Mock(return_value=mock_download_manager)) }): from src.server.web.controllers.api.v1.episodes import episodes_bp except ImportError: episodes_bp = None class TestEpisodeEndpoints: """Test cases for episode API endpoints.""" @pytest.fixture def app(self): """Create a test Flask application.""" if not episodes_bp: pytest.skip("Module not available") app = Flask(__name__) app.config['TESTING'] = True app.register_blueprint(episodes_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_episode_manager.reset_mock() mock_anime_manager.reset_mock() mock_download_manager.reset_mock() def test_list_episodes_success(self, client, mock_session): """Test GET /episodes - list episodes with pagination.""" if not episodes_bp: pytest.skip("Module not available") mock_episodes = [ { 'id': 1, 'anime_id': 1, 'number': 1, 'title': 'Episode 1', 'url': 'https://example.com/episode/1', 'status': 'available' }, { 'id': 2, 'anime_id': 1, 'number': 2, 'title': 'Episode 2', 'url': 'https://example.com/episode/2', 'status': 'available' } ] mock_episode_manager.get_all_episodes.return_value = mock_episodes mock_episode_manager.get_episodes_count.return_value = 2 response = client.get('/api/v1/episodes?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]['number'] == 1 mock_episode_manager.get_all_episodes.assert_called_once_with( offset=0, limit=10, anime_id=None, status=None, sort_by='number', sort_order='asc' ) def test_list_episodes_with_filters(self, client, mock_session): """Test GET /episodes with filters.""" if not episodes_bp: pytest.skip("Module not available") mock_episode_manager.get_all_episodes.return_value = [] mock_episode_manager.get_episodes_count.return_value = 0 response = client.get('/api/v1/episodes?anime_id=1&status=downloaded&sort_by=title&sort_order=desc') assert response.status_code == 200 mock_episode_manager.get_all_episodes.assert_called_once_with( offset=0, limit=20, anime_id=1, status='downloaded', sort_by='title', sort_order='desc' ) def test_get_episode_by_id_success(self, client, mock_session): """Test GET /episodes/ - get specific episode.""" if not episodes_bp: pytest.skip("Module not available") mock_episode = { 'id': 1, 'anime_id': 1, 'number': 1, 'title': 'First Episode', 'url': 'https://example.com/episode/1', 'status': 'available', 'duration': 1440, 'description': 'The first episode' } mock_episode_manager.get_episode_by_id.return_value = mock_episode response = client.get('/api/v1/episodes/1') assert response.status_code == 200 data = json.loads(response.data) assert data['success'] is True assert data['data']['id'] == 1 assert data['data']['title'] == 'First Episode' mock_episode_manager.get_episode_by_id.assert_called_once_with(1) def test_get_episode_by_id_not_found(self, client, mock_session): """Test GET /episodes/ - episode not found.""" if not episodes_bp: pytest.skip("Module not available") mock_episode_manager.get_episode_by_id.return_value = None response = client.get('/api/v1/episodes/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_episode_success(self, client, mock_session): """Test POST /episodes - create new episode.""" if not episodes_bp: pytest.skip("Module not available") episode_data = { 'anime_id': 1, 'number': 1, 'title': 'New Episode', 'url': 'https://example.com/new-episode', 'duration': 1440, 'description': 'A new episode' } # Mock anime exists mock_anime_manager.get_anime_by_id.return_value = {'id': 1, 'name': 'Test Anime'} mock_episode_manager.create_episode.return_value = 1 mock_episode_manager.get_episode_by_id.return_value = { 'id': 1, **episode_data } response = client.post('/api/v1/episodes', json=episode_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']['title'] == 'New Episode' mock_episode_manager.create_episode.assert_called_once() def test_create_episode_invalid_anime(self, client, mock_session): """Test POST /episodes - invalid anime_id.""" if not episodes_bp: pytest.skip("Module not available") episode_data = { 'anime_id': 999, 'number': 1, 'title': 'New Episode', 'url': 'https://example.com/new-episode' } # Mock anime doesn't exist mock_anime_manager.get_anime_by_id.return_value = None response = client.post('/api/v1/episodes', json=episode_data, content_type='application/json') assert response.status_code == 400 data = json.loads(response.data) assert 'error' in data assert 'anime' in data['error'].lower() def test_create_episode_validation_error(self, client, mock_session): """Test POST /episodes - validation error.""" if not episodes_bp: pytest.skip("Module not available") # Missing required fields episode_data = { 'title': 'New Episode' } response = client.post('/api/v1/episodes', json=episode_data, content_type='application/json') assert response.status_code == 400 data = json.loads(response.data) assert 'error' in data def test_update_episode_success(self, client, mock_session): """Test PUT /episodes/ - update episode.""" if not episodes_bp: pytest.skip("Module not available") update_data = { 'title': 'Updated Episode', 'description': 'Updated description', 'status': 'downloaded' } mock_episode_manager.get_episode_by_id.return_value = { 'id': 1, 'title': 'Original Episode' } mock_episode_manager.update_episode.return_value = True response = client.put('/api/v1/episodes/1', json=update_data, content_type='application/json') assert response.status_code == 200 data = json.loads(response.data) assert data['success'] is True mock_episode_manager.update_episode.assert_called_once_with(1, update_data) def test_update_episode_not_found(self, client, mock_session): """Test PUT /episodes/ - episode not found.""" if not episodes_bp: pytest.skip("Module not available") mock_episode_manager.get_episode_by_id.return_value = None response = client.put('/api/v1/episodes/999', json={'title': 'Updated'}, content_type='application/json') assert response.status_code == 404 data = json.loads(response.data) assert 'error' in data def test_delete_episode_success(self, client, mock_session): """Test DELETE /episodes/ - delete episode.""" if not episodes_bp: pytest.skip("Module not available") mock_episode_manager.get_episode_by_id.return_value = { 'id': 1, 'title': 'Test Episode' } mock_episode_manager.delete_episode.return_value = True response = client.delete('/api/v1/episodes/1') assert response.status_code == 200 data = json.loads(response.data) assert data['success'] is True mock_episode_manager.delete_episode.assert_called_once_with(1) def test_delete_episode_not_found(self, client, mock_session): """Test DELETE /episodes/ - episode not found.""" if not episodes_bp: pytest.skip("Module not available") mock_episode_manager.get_episode_by_id.return_value = None response = client.delete('/api/v1/episodes/999') assert response.status_code == 404 data = json.loads(response.data) assert 'error' in data def test_bulk_create_episodes_success(self, client, mock_session): """Test POST /episodes/bulk - bulk create episodes.""" if not episodes_bp: pytest.skip("Module not available") bulk_data = { 'episodes': [ {'anime_id': 1, 'number': 1, 'title': 'Episode 1', 'url': 'https://example.com/1'}, {'anime_id': 1, 'number': 2, 'title': 'Episode 2', 'url': 'https://example.com/2'} ] } mock_episode_manager.bulk_create_episodes.return_value = { 'created': 2, 'failed': 0, 'created_ids': [1, 2] } response = client.post('/api/v1/episodes/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'] == 2 assert data['data']['failed'] == 0 def test_bulk_update_status_success(self, client, mock_session): """Test PUT /episodes/bulk/status - bulk update episode status.""" if not episodes_bp: pytest.skip("Module not available") bulk_data = { 'episode_ids': [1, 2, 3], 'status': 'downloaded' } mock_episode_manager.bulk_update_status.return_value = { 'updated': 3, 'failed': 0 } response = client.put('/api/v1/episodes/bulk/status', 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']['updated'] == 3 def test_bulk_delete_episodes_success(self, client, mock_session): """Test DELETE /episodes/bulk - bulk delete episodes.""" if not episodes_bp: pytest.skip("Module not available") bulk_data = { 'episode_ids': [1, 2, 3] } mock_episode_manager.bulk_delete_episodes.return_value = { 'deleted': 3, 'failed': 0 } response = client.delete('/api/v1/episodes/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']['deleted'] == 3 def test_sync_episodes_success(self, client, mock_session): """Test POST /episodes/sync - sync episodes for anime.""" if not episodes_bp: pytest.skip("Module not available") sync_data = { 'anime_id': 1 } # Mock anime exists mock_anime_manager.get_anime_by_id.return_value = {'id': 1, 'name': 'Test Anime'} mock_episode_manager.sync_episodes.return_value = { 'anime_id': 1, 'episodes_found': 12, 'episodes_added': 5, 'episodes_updated': 2 } response = client.post('/api/v1/episodes/sync', json=sync_data, content_type='application/json') assert response.status_code == 200 data = json.loads(response.data) assert data['success'] is True assert data['data']['episodes_found'] == 12 assert data['data']['episodes_added'] == 5 mock_episode_manager.sync_episodes.assert_called_once_with(1) def test_sync_episodes_invalid_anime(self, client, mock_session): """Test POST /episodes/sync - invalid anime_id.""" if not episodes_bp: pytest.skip("Module not available") sync_data = { 'anime_id': 999 } # Mock anime doesn't exist mock_anime_manager.get_anime_by_id.return_value = None response = client.post('/api/v1/episodes/sync', json=sync_data, content_type='application/json') assert response.status_code == 400 data = json.loads(response.data) assert 'error' in data assert 'anime' in data['error'].lower() def test_get_episode_download_info_success(self, client, mock_session): """Test GET /episodes//download - get episode download info.""" if not episodes_bp: pytest.skip("Module not available") mock_episode_manager.get_episode_by_id.return_value = { 'id': 1, 'title': 'Test Episode' } mock_download_info = { 'episode_id': 1, 'download_id': 5, 'status': 'downloading', 'progress': 45.5, 'speed': 1048576, # 1MB/s 'eta': 300, # 5 minutes 'file_path': '/downloads/episode1.mp4' } mock_download_manager.get_episode_download_info.return_value = mock_download_info response = client.get('/api/v1/episodes/1/download') assert response.status_code == 200 data = json.loads(response.data) assert data['success'] is True assert data['data']['status'] == 'downloading' assert data['data']['progress'] == 45.5 def test_get_episode_download_info_not_downloading(self, client, mock_session): """Test GET /episodes//download - episode not downloading.""" if not episodes_bp: pytest.skip("Module not available") mock_episode_manager.get_episode_by_id.return_value = { 'id': 1, 'title': 'Test Episode' } mock_download_manager.get_episode_download_info.return_value = None response = client.get('/api/v1/episodes/1/download') assert response.status_code == 404 data = json.loads(response.data) assert 'error' in data assert 'download' in data['error'].lower() def test_start_episode_download_success(self, client, mock_session): """Test POST /episodes//download - start episode download.""" if not episodes_bp: pytest.skip("Module not available") mock_episode_manager.get_episode_by_id.return_value = { 'id': 1, 'title': 'Test Episode', 'url': 'https://example.com/episode/1' } mock_download_manager.start_episode_download.return_value = { 'download_id': 5, 'status': 'queued', 'message': 'Download queued successfully' } response = client.post('/api/v1/episodes/1/download') assert response.status_code == 200 data = json.loads(response.data) assert data['success'] is True assert data['data']['download_id'] == 5 assert data['data']['status'] == 'queued' mock_download_manager.start_episode_download.assert_called_once_with(1) def test_cancel_episode_download_success(self, client, mock_session): """Test DELETE /episodes//download - cancel episode download.""" if not episodes_bp: pytest.skip("Module not available") mock_episode_manager.get_episode_by_id.return_value = { 'id': 1, 'title': 'Test Episode' } mock_download_manager.cancel_episode_download.return_value = True response = client.delete('/api/v1/episodes/1/download') assert response.status_code == 200 data = json.loads(response.data) assert data['success'] is True mock_download_manager.cancel_episode_download.assert_called_once_with(1) def test_get_episodes_by_anime_success(self, client, mock_session): """Test GET /anime//episodes - get episodes for anime.""" if not episodes_bp: pytest.skip("Module not available") mock_anime_manager.get_anime_by_id.return_value = { 'id': 1, 'name': 'Test Anime' } mock_episodes = [ {'id': 1, 'number': 1, 'title': 'Episode 1'}, {'id': 2, 'number': 2, 'title': 'Episode 2'} ] mock_episode_manager.get_episodes_by_anime.return_value = mock_episodes response = client.get('/api/v1/anime/1/episodes') assert response.status_code == 200 data = json.loads(response.data) assert data['success'] is True assert len(data['data']) == 2 assert data['data'][0]['number'] == 1 mock_episode_manager.get_episodes_by_anime.assert_called_once_with(1) class TestEpisodeAuthentication: """Test cases for episode endpoints authentication.""" @pytest.fixture def app(self): """Create a test Flask application.""" if not episodes_bp: pytest.skip("Module not available") app = Flask(__name__) app.config['TESTING'] = True app.register_blueprint(episodes_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 episodes_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_episode_manager.get_all_episodes.return_value = [] mock_episode_manager.get_episodes_count.return_value = 0 response = client.get('/api/v1/episodes') # 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 episodes_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/episodes', json={'title': 'Test'}, content_type='application/json') # Should require authentication for write operations assert response.status_code == 401 class TestEpisodeErrorHandling: """Test cases for episode endpoints error handling.""" @pytest.fixture def app(self): """Create a test Flask application.""" if not episodes_bp: pytest.skip("Module not available") app = Flask(__name__) app.config['TESTING'] = True app.register_blueprint(episodes_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 episodes_bp: pytest.skip("Module not available") # Simulate database error mock_episode_manager.get_all_episodes.side_effect = Exception("Database connection failed") response = client.get('/api/v1/episodes') assert response.status_code == 500 data = json.loads(response.data) assert 'error' in data def test_invalid_episode_id_parameter(self, client, mock_session): """Test handling of invalid episode ID parameter.""" if not episodes_bp: pytest.skip("Module not available") response = client.get('/api/v1/episodes/invalid-id') assert response.status_code == 404 # Flask will handle this as route not found def test_concurrent_modification_error(self, client, mock_session): """Test handling of concurrent modification errors.""" if not episodes_bp: pytest.skip("Module not available") mock_episode_manager.get_episode_by_id.return_value = { 'id': 1, 'title': 'Test Episode' } # Simulate concurrent modification mock_episode_manager.update_episode.side_effect = Exception("Episode was modified by another process") response = client.put('/api/v1/episodes/1', json={'title': 'Updated'}, content_type='application/json') assert response.status_code == 500 data = json.loads(response.data) assert 'error' in data if __name__ == '__main__': pytest.main([__file__])