557 lines
19 KiB
Python
557 lines
19 KiB
Python
"""
|
|
Test cases for anime API endpoints.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
from flask import Flask
|
|
import json
|
|
|
|
# Mock the database managers first
|
|
mock_anime_manager = Mock()
|
|
mock_download_manager = Mock()
|
|
|
|
# Import the modules to test
|
|
try:
|
|
with patch.dict('sys.modules', {
|
|
'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.anime import anime_bp
|
|
except ImportError:
|
|
anime_bp = None
|
|
|
|
|
|
class TestAnimeEndpoints:
|
|
"""Test cases for anime API endpoints."""
|
|
|
|
@pytest.fixture
|
|
def app(self):
|
|
"""Create a test Flask application."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
app = Flask(__name__)
|
|
app.config['TESTING'] = True
|
|
app.register_blueprint(anime_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_anime_manager.reset_mock()
|
|
mock_download_manager.reset_mock()
|
|
|
|
def test_list_anime_success(self, client, mock_session):
|
|
"""Test GET /anime - list anime with pagination."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
# Mock anime data
|
|
mock_anime_list = [
|
|
{'id': 1, 'name': 'Anime 1', 'url': 'https://example.com/1'},
|
|
{'id': 2, 'name': 'Anime 2', 'url': 'https://example.com/2'}
|
|
]
|
|
|
|
mock_anime_manager.get_all_anime.return_value = mock_anime_list
|
|
mock_anime_manager.get_anime_count.return_value = 2
|
|
|
|
response = client.get('/api/v1/anime?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
|
|
|
|
# Verify manager was called with correct parameters
|
|
mock_anime_manager.get_all_anime.assert_called_once_with(
|
|
offset=0, limit=10, search=None, status=None, sort_by='name', sort_order='asc'
|
|
)
|
|
|
|
def test_list_anime_with_search(self, client, mock_session):
|
|
"""Test GET /anime with search parameter."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
mock_anime_manager.get_all_anime.return_value = []
|
|
mock_anime_manager.get_anime_count.return_value = 0
|
|
|
|
response = client.get('/api/v1/anime?search=naruto&status=completed')
|
|
|
|
assert response.status_code == 200
|
|
mock_anime_manager.get_all_anime.assert_called_once_with(
|
|
offset=0, limit=20, search='naruto', status='completed', sort_by='name', sort_order='asc'
|
|
)
|
|
|
|
def test_get_anime_by_id_success(self, client, mock_session):
|
|
"""Test GET /anime/<id> - get specific anime."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
mock_anime = {
|
|
'id': 1,
|
|
'name': 'Test Anime',
|
|
'url': 'https://example.com/1',
|
|
'description': 'A test anime'
|
|
}
|
|
|
|
mock_anime_manager.get_anime_by_id.return_value = mock_anime
|
|
|
|
response = client.get('/api/v1/anime/1')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data['data']['id'] == 1
|
|
assert data['data']['name'] == 'Test Anime'
|
|
|
|
mock_anime_manager.get_anime_by_id.assert_called_once_with(1)
|
|
|
|
def test_get_anime_by_id_not_found(self, client, mock_session):
|
|
"""Test GET /anime/<id> - anime not found."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
mock_anime_manager.get_anime_by_id.return_value = None
|
|
|
|
response = client.get('/api/v1/anime/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_anime_success(self, client, mock_session):
|
|
"""Test POST /anime - create new anime."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
anime_data = {
|
|
'name': 'New Anime',
|
|
'url': 'https://example.com/new-anime',
|
|
'description': 'A new anime',
|
|
'episodes': 12,
|
|
'status': 'ongoing'
|
|
}
|
|
|
|
mock_anime_manager.create_anime.return_value = 1
|
|
mock_anime_manager.get_anime_by_id.return_value = {
|
|
'id': 1,
|
|
**anime_data
|
|
}
|
|
|
|
response = client.post('/api/v1/anime',
|
|
json=anime_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']['name'] == 'New Anime'
|
|
|
|
mock_anime_manager.create_anime.assert_called_once()
|
|
|
|
def test_create_anime_validation_error(self, client, mock_session):
|
|
"""Test POST /anime - validation error."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
# Missing required fields
|
|
anime_data = {
|
|
'description': 'A new anime'
|
|
}
|
|
|
|
response = client.post('/api/v1/anime',
|
|
json=anime_data,
|
|
content_type='application/json')
|
|
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_create_anime_duplicate(self, client, mock_session):
|
|
"""Test POST /anime - duplicate anime."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
anime_data = {
|
|
'name': 'Existing Anime',
|
|
'url': 'https://example.com/existing'
|
|
}
|
|
|
|
# Simulate duplicate error
|
|
mock_anime_manager.create_anime.side_effect = Exception("Duplicate entry")
|
|
|
|
response = client.post('/api/v1/anime',
|
|
json=anime_data,
|
|
content_type='application/json')
|
|
|
|
assert response.status_code == 500
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_update_anime_success(self, client, mock_session):
|
|
"""Test PUT /anime/<id> - update anime."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
update_data = {
|
|
'name': 'Updated Anime',
|
|
'description': 'Updated description',
|
|
'status': 'completed'
|
|
}
|
|
|
|
mock_anime_manager.get_anime_by_id.return_value = {
|
|
'id': 1,
|
|
'name': 'Original Anime'
|
|
}
|
|
mock_anime_manager.update_anime.return_value = True
|
|
|
|
response = client.put('/api/v1/anime/1',
|
|
json=update_data,
|
|
content_type='application/json')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data['success'] is True
|
|
|
|
mock_anime_manager.update_anime.assert_called_once_with(1, update_data)
|
|
|
|
def test_update_anime_not_found(self, client, mock_session):
|
|
"""Test PUT /anime/<id> - anime not found."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
mock_anime_manager.get_anime_by_id.return_value = None
|
|
|
|
response = client.put('/api/v1/anime/999',
|
|
json={'name': 'Updated'},
|
|
content_type='application/json')
|
|
|
|
assert response.status_code == 404
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_delete_anime_success(self, client, mock_session):
|
|
"""Test DELETE /anime/<id> - delete anime."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
mock_anime_manager.get_anime_by_id.return_value = {
|
|
'id': 1,
|
|
'name': 'Test Anime'
|
|
}
|
|
mock_anime_manager.delete_anime.return_value = True
|
|
|
|
response = client.delete('/api/v1/anime/1')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data['success'] is True
|
|
|
|
mock_anime_manager.delete_anime.assert_called_once_with(1)
|
|
|
|
def test_delete_anime_not_found(self, client, mock_session):
|
|
"""Test DELETE /anime/<id> - anime not found."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
mock_anime_manager.get_anime_by_id.return_value = None
|
|
|
|
response = client.delete('/api/v1/anime/999')
|
|
|
|
assert response.status_code == 404
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_bulk_create_anime_success(self, client, mock_session):
|
|
"""Test POST /anime/bulk - bulk create anime."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
bulk_data = {
|
|
'anime_list': [
|
|
{'name': 'Anime 1', 'url': 'https://example.com/1'},
|
|
{'name': 'Anime 2', 'url': 'https://example.com/2'}
|
|
]
|
|
}
|
|
|
|
mock_anime_manager.bulk_create_anime.return_value = {
|
|
'created': 2,
|
|
'failed': 0,
|
|
'created_ids': [1, 2]
|
|
}
|
|
|
|
response = client.post('/api/v1/anime/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_anime_success(self, client, mock_session):
|
|
"""Test PUT /anime/bulk - bulk update anime."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
bulk_data = {
|
|
'updates': [
|
|
{'id': 1, 'status': 'completed'},
|
|
{'id': 2, 'status': 'completed'}
|
|
]
|
|
}
|
|
|
|
mock_anime_manager.bulk_update_anime.return_value = {
|
|
'updated': 2,
|
|
'failed': 0
|
|
}
|
|
|
|
response = client.put('/api/v1/anime/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']['updated'] == 2
|
|
|
|
def test_bulk_delete_anime_success(self, client, mock_session):
|
|
"""Test DELETE /anime/bulk - bulk delete anime."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
bulk_data = {
|
|
'anime_ids': [1, 2, 3]
|
|
}
|
|
|
|
mock_anime_manager.bulk_delete_anime.return_value = {
|
|
'deleted': 3,
|
|
'failed': 0
|
|
}
|
|
|
|
response = client.delete('/api/v1/anime/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_get_anime_episodes_success(self, client, mock_session):
|
|
"""Test GET /anime/<id>/episodes - get anime episodes."""
|
|
if not anime_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_anime_manager.get_anime_episodes.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
|
|
|
|
def test_get_anime_stats_success(self, client, mock_session):
|
|
"""Test GET /anime/<id>/stats - get anime statistics."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
mock_anime_manager.get_anime_by_id.return_value = {
|
|
'id': 1,
|
|
'name': 'Test Anime'
|
|
}
|
|
|
|
mock_stats = {
|
|
'total_episodes': 12,
|
|
'downloaded_episodes': 8,
|
|
'download_progress': 66.7,
|
|
'total_size': 1073741824, # 1GB
|
|
'downloaded_size': 715827882 # ~680MB
|
|
}
|
|
|
|
mock_anime_manager.get_anime_stats.return_value = mock_stats
|
|
|
|
response = client.get('/api/v1/anime/1/stats')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data['success'] is True
|
|
assert data['data']['total_episodes'] == 12
|
|
assert data['data']['downloaded_episodes'] == 8
|
|
|
|
def test_search_anime_success(self, client, mock_session):
|
|
"""Test GET /anime/search - search anime."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
mock_results = [
|
|
{'id': 1, 'name': 'Naruto', 'url': 'https://example.com/naruto'},
|
|
{'id': 2, 'name': 'Naruto Shippuden', 'url': 'https://example.com/naruto-shippuden'}
|
|
]
|
|
|
|
mock_anime_manager.search_anime.return_value = mock_results
|
|
|
|
response = client.get('/api/v1/anime/search?q=naruto')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data['success'] is True
|
|
assert len(data['data']) == 2
|
|
assert 'naruto' in data['data'][0]['name'].lower()
|
|
|
|
mock_anime_manager.search_anime.assert_called_once_with('naruto', limit=20)
|
|
|
|
def test_search_anime_no_query(self, client, mock_session):
|
|
"""Test GET /anime/search - missing search query."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
response = client.get('/api/v1/anime/search')
|
|
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
assert 'query parameter' in data['error'].lower()
|
|
|
|
|
|
class TestAnimeAuthentication:
|
|
"""Test cases for anime endpoints authentication."""
|
|
|
|
@pytest.fixture
|
|
def app(self):
|
|
"""Create a test Flask application."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
app = Flask(__name__)
|
|
app.config['TESTING'] = True
|
|
app.register_blueprint(anime_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 anime_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_anime_manager.get_all_anime.return_value = []
|
|
mock_anime_manager.get_anime_count.return_value = 0
|
|
|
|
response = client.get('/api/v1/anime')
|
|
# Should still work for read operations
|
|
assert response.status_code == 200
|
|
|
|
def test_authenticated_write_access(self, client):
|
|
"""Test that write operations require authentication."""
|
|
if not anime_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/anime',
|
|
json={'name': 'Test'},
|
|
content_type='application/json')
|
|
# Should require authentication for write operations
|
|
assert response.status_code == 401
|
|
|
|
|
|
class TestAnimeErrorHandling:
|
|
"""Test cases for anime endpoints error handling."""
|
|
|
|
@pytest.fixture
|
|
def app(self):
|
|
"""Create a test Flask application."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
app = Flask(__name__)
|
|
app.config['TESTING'] = True
|
|
app.register_blueprint(anime_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 anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
# Simulate database error
|
|
mock_anime_manager.get_all_anime.side_effect = Exception("Database connection failed")
|
|
|
|
response = client.get('/api/v1/anime')
|
|
|
|
assert response.status_code == 500
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_invalid_json_handling(self, client, mock_session):
|
|
"""Test handling of invalid JSON."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
response = client.post('/api/v1/anime',
|
|
data='invalid json',
|
|
content_type='application/json')
|
|
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_method_not_allowed(self, client):
|
|
"""Test method not allowed responses."""
|
|
if not anime_bp:
|
|
pytest.skip("Module not available")
|
|
|
|
response = client.patch('/api/v1/anime/1')
|
|
|
|
assert response.status_code == 405
|
|
|
|
|
|
if __name__ == '__main__':
|
|
pytest.main([__file__]) |