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

679 lines
24 KiB
Python

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